/*
* Copyright (C) 2005 Jesper Steen Møller
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.alfresco.repo.action.executer;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.metadata.AbstractMappingMetadataExtracter;
import org.alfresco.repo.content.metadata.MetadataExtracter;
import org.alfresco.repo.content.metadata.MetadataExtracterRegistry;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Extract metadata from any added content.
*
* Currently, the default {@linkplain MetadataExtracter.OverwritePolicy overwrite policy}
* for each extracter is used. (TODO: Add overwrite policy as a parameter.)
*
* @see MetadataExtracter.OverwritePolicy
*
* @author Jesper Steen Møller
*/
public class ContentMetadataExtracter extends ActionExecuterAbstractBase
{
private static Log logger = LogFactory.getLog(ContentMetadataExtracter.class);
public static final String EXECUTOR_NAME = "extract-metadata";
private NodeService nodeService;
private ContentService contentService;
private DictionaryService dictionaryService;
private TaggingService taggingService;
private MetadataExtracterRegistry metadataExtracterRegistry;
private boolean carryAspectProperties = true;
private boolean enableStringTagging = false;
public ContentMetadataExtracter()
{
}
/**
* @param nodeService the node service
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* @param contentService The contentService to set.
*/
public void setContentService(ContentService contentService)
{
this.contentService = contentService;
}
/**
* @param dictService The DictionaryService to set.
*/
public void setDictionaryService(DictionaryService dictService)
{
this.dictionaryService = dictService;
}
/**
* @param taggingService The TaggingService to set.
*/
public void setTaggingService(TaggingService taggingService)
{
this.taggingService = taggingService;
}
/**
* @param metadataExtracterRegistry The metadataExtracterRegistry to set.
*/
public void setMetadataExtracterRegistry(MetadataExtracterRegistry metadataExtracterRegistry)
{
this.metadataExtracterRegistry = metadataExtracterRegistry;
}
/**
* Whether or not aspect-related properties must be carried to the new version of the node
*
* @param carryAspectProperties true (default) to carry all aspect-linked
* properties forward. false will clean the
* aspect of any unextracted values.
*/
public void setCarryAspectProperties(boolean carryAspectProperties)
{
this.carryAspectProperties = carryAspectProperties;
}
/**
* Whether or not to enable mapping of simple strings to cm:taggable tags
*
* @param enableStringTagging true find or create tags for each string
* mapped to cm:taggable. false (default)
* ignore mapping strings to tags.
*/
public void setEnableStringTagging(boolean enableStringTagging)
{
this.enableStringTagging = enableStringTagging;
}
/**
* Iterates the values of the taggable property which the metadata
* extractor should have already attempted to convert values to {@link NodeRef}s.
*
* If conversion by the metadata extracter failed due to a MalformedNodeRefException
* the taggable property should still contain raw string values.
*
* Mixing of NodeRefs and string values is permitted so each raw value is
* checked for a valid NodeRef representation and if so, converts to a NodeRef,
* if not, adds as a tag via the {@link TaggingService}.
*
* @param actionedUponNodeRef The NodeRef being actioned upon
* @param propertyDef the PropertyDefinition of the taggable property
* @param rawValue the raw value from the metadata extracter
*/
@SuppressWarnings("unchecked")
protected void addTags(NodeRef actionedUponNodeRef, PropertyDefinition propertyDef, Serializable rawValue)
{
List tags = new ArrayList();
if (logger.isDebugEnabled())
{
logger.debug("converting " + rawValue.toString() + " of type " +
rawValue.getClass().getCanonicalName() + " to tags");
}
if (rawValue instanceof Collection>)
{
for (Object singleValue : (Collection>) rawValue)
{
if (singleValue instanceof String)
{
if (NodeRef.isNodeRef((String) singleValue))
{
// Convert to a NodeRef
Serializable convertedPropertyValue = (Serializable) DefaultTypeConverter.INSTANCE.convert(
propertyDef.getDataType(),
(String) singleValue);
try {
String tagName = (String) nodeService.getProperty((NodeRef) convertedPropertyValue, ContentModel.PROP_NAME);
if (logger.isTraceEnabled())
{
logger.trace("found tag '" + tagName + "' from tag nodeRef '" + (String) singleValue + "', " +
"adding to " + actionedUponNodeRef.toString());
}
if (tagName != null && !tagName.equals(""))
{
tags.add(tagName);
}
}
catch (InvalidNodeRefException e)
{
if (logger.isWarnEnabled())
{
logger.warn("tag nodeRef Invalid: " + e.getMessage());
}
}
}
else
{
// Must be a simple string
if (logger.isTraceEnabled())
{
logger.trace("adding string tag '" + (String) singleValue + "' to " + actionedUponNodeRef.toString());
}
tags.add((String) singleValue);
}
}
else if (singleValue instanceof NodeRef)
{
String tagName = (String) nodeService.getProperty((NodeRef) singleValue, ContentModel.PROP_NAME);
tags.add(tagName);
}
}
}
else if (rawValue instanceof String)
{
if (logger.isTraceEnabled())
{
logger.trace("adding tag '" + (String) rawValue + "' to " + actionedUponNodeRef.toString());
}
tags.add((String) rawValue);
}
taggingService.addTags(actionedUponNodeRef, tags);
}
/**
* @see org.alfresco.repo.action.executer.ActionExecuter#execute(org.alfresco.service.cmr.repository.NodeRef,
* NodeRef)
*/
public void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef)
{
if (!nodeService.exists(actionedUponNodeRef))
{
// Node is gone
return;
}
ContentReader reader = contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT);
// The reader may be null, e.g. for folders and the like
if (reader == null || reader.getMimetype() == null)
{
if(logger.isDebugEnabled())
{
logger.debug("no content or mimetype - do nothing");
}
// No content to extract data from
return;
}
String mimetype = reader.getMimetype();
MetadataExtracter extracter = metadataExtracterRegistry.getExtracter(mimetype);
if (extracter == null)
{
if(logger.isDebugEnabled())
{
logger.debug("no extracter for mimetype:" + mimetype);
}
// There is no extracter to use
return;
}
if (enableStringTagging && (extracter instanceof AbstractMappingMetadataExtracter))
{
((AbstractMappingMetadataExtracter) extracter).setEnableStringTagging(enableStringTagging);
}
// Get all the node's properties
Map nodeProperties = nodeService.getProperties(actionedUponNodeRef);
// TODO: The override policy should be a parameter here. Instead, we'll use the default policy
// set on the extracter.
// Give the node's properties to the extracter to be modified
Map modifiedProperties = null;
try
{
modifiedProperties = extracter.extract(
reader,
/*OverwritePolicy.PRAGMATIC,*/
nodeProperties);
}
catch (Throwable e)
{
// Extracters should attempt to handle all error conditions and extract
// as much as they can. If, however, one should fail, we don't want the
// action itself to fail. We absorb and report the exception here to
// solve ETHREEOH-1936 and ALFCOM-2889.
if (logger.isDebugEnabled())
{
logger.debug(
"Raw metadata extraction failed: \n" +
" Extracter: " + this + "\n" +
" Node: " + actionedUponNodeRef + "\n" +
" Content: " + reader,
e);
}
else
{
logger.warn(
"Raw metadata extraction failed (turn on DEBUG for full error): \n" +
" Extracter: " + this + "\n" +
" Node: " + actionedUponNodeRef + "\n" +
" Content: " + reader + "\n" +
" Failure: " + e.getMessage());
}
modifiedProperties = new HashMap(0);
}
// If none of the properties where changed, then there is nothing more to do
if (modifiedProperties.size() == 0)
{
return;
}
// Check that all properties have the appropriate aspect applied
Set requiredAspectQNames = new HashSet(3);
Set aspectPropertyQNames = new HashSet(17);
/**
* The modified properties contain null values as well. As we are only interested
* in the keys, this will force aspect aspect properties to be removed even if there
* are no settable properties pertaining to the aspect.
*/
for (QName propertyQName : modifiedProperties.keySet())
{
PropertyDefinition propertyDef = dictionaryService.getProperty(propertyQName);
if (propertyDef == null)
{
// The property is not defined in the model
continue;
}
ClassDefinition propertyContainerDef = propertyDef.getContainerClass();
if (propertyContainerDef.isAspect())
{
if (enableStringTagging && propertyContainerDef.getName().equals(ContentModel.ASPECT_TAGGABLE))
{
Serializable oldValue = nodeProperties.get(propertyQName);
addTags(actionedUponNodeRef, propertyDef, oldValue);
// Replace the raw value with the created tag NodeRefs
nodeProperties.put(ContentModel.PROP_TAGS,
nodeService.getProperty(actionedUponNodeRef, ContentModel.PROP_TAGS));
}
else
{
QName aspectQName = propertyContainerDef.getName();
requiredAspectQNames.add(aspectQName);
// Get all properties associated with the aspect
Set aspectProperties = propertyContainerDef.getProperties().keySet();
aspectPropertyQNames.addAll(aspectProperties);
}
}
}
if (!carryAspectProperties)
{
// Remove any node properties that are defined on the aspects but were not extracted
for (QName aspectPropertyQName : aspectPropertyQNames)
{
if (!modifiedProperties.containsKey(aspectPropertyQName))
{
// Simple case: This property was not extracted
nodeProperties.remove(aspectPropertyQName);
}
else if (modifiedProperties.get(aspectPropertyQName) == null)
{
// Trickier (ALF-1823): The property was extracted as 'null'
nodeProperties.remove(aspectPropertyQName);
}
}
}
// Add all the properties to the node BEFORE we add the aspects
nodeService.setProperties(actionedUponNodeRef, nodeProperties);
// Add each of the aspects, as required
for (QName requiredAspectQName : requiredAspectQNames)
{
if (nodeService.hasAspect(actionedUponNodeRef, requiredAspectQName))
{
// The node has the aspect already
continue;
}
else
{
nodeService.addAspect(actionedUponNodeRef, requiredAspectQName, null);
}
}
}
@Override
protected void addParameterDefinitions(List arg0)
{
// None!
}
}