diff --git a/config/alfresco/action-services-context.xml b/config/alfresco/action-services-context.xml
index 7bbe04d743..249ebbebb0 100644
--- a/config/alfresco/action-services-context.xml
+++ b/config/alfresco/action-services-context.xml
@@ -539,6 +539,23 @@
true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {http://www.alfresco.org/model/content/1.0}content
+
+
+
diff --git a/config/alfresco/messages/action-config.properties b/config/alfresco/messages/action-config.properties
index 8ac3011581..c577fc88db 100644
--- a/config/alfresco/messages/action-config.properties
+++ b/config/alfresco/messages/action-config.properties
@@ -148,6 +148,9 @@ import.destination.display-label=Destination
extract-metadata.title=Extract common metadata fields
extract-metadata.description=Imports title, author and description metadata fields from common content types.
+embed-metadata.title=Embed properties as metadata in content
+embed-metadata.description=This action attempts to embed the content's properties as metadata in the file itself
+
specialise-type.title=Specialise type
specialise-type.description=This will specialise the matched item to a given type.
specialise-type.type-name.display-label=Type
diff --git a/source/java/org/alfresco/repo/action/executer/ContentMetadataEmbedder.java b/source/java/org/alfresco/repo/action/executer/ContentMetadataEmbedder.java
new file mode 100644
index 0000000000..be59ff45be
--- /dev/null
+++ b/source/java/org/alfresco/repo/action/executer/ContentMetadataEmbedder.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2005-2012 Alfresco Software Limited.
+ *
+ * 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.List;
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
+import org.alfresco.repo.content.metadata.MetadataEmbedder;
+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.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentService;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.namespace.QName;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Embed metadata in any content.
+ *
+ * The metadata is embedded in the content from the current
+ * property values.
+ *
+ * @author Jesper Steen Møller, Ray Gauss II
+ */
+public class ContentMetadataEmbedder extends ActionExecuterAbstractBase
+{
+ private static Log logger = LogFactory.getLog(ContentMetadataEmbedder.class);
+
+ public static final String EXECUTOR_NAME = "embed-metadata";
+
+ private NodeService nodeService;
+ private ContentService contentService;
+ private MetadataExtracterRegistry metadataExtracterRegistry;
+
+ /**
+ * @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 metadataExtracterRegistry The metadataExtracterRegistry to set.
+ */
+ public void setMetadataExtracterRegistry(MetadataExtracterRegistry metadataExtracterRegistry)
+ {
+ this.metadataExtracterRegistry = metadataExtracterRegistry;
+ }
+
+ /**
+ * @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();
+ MetadataEmbedder embedder = metadataExtracterRegistry.getEmbedder(mimetype);
+ if (embedder == null)
+ {
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("no embedder for mimetype:" + mimetype);
+ }
+ // There is no embedder to use
+ return;
+ }
+
+ ContentWriter writer = contentService.getWriter(actionedUponNodeRef, ContentModel.PROP_CONTENT, true);
+ // The writer may be null, e.g. for folders and the like
+ if (writer == null || writer.getMimetype() == null)
+ {
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("no content or mimetype - do nothing");
+ }
+ // No content to embed data in
+ return;
+ }
+
+ // Get all the node's properties
+ Map nodeProperties = nodeService.getProperties(actionedUponNodeRef);
+
+ try
+ {
+ embedder.embed(nodeProperties, reader, writer);
+ }
+ catch (Throwable e)
+ {
+ // Extracters should attempt to handle all error conditions and embed
+ // 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.
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Meetadata embedding failed: \n" +
+ " Extracter: " + this + "\n" +
+ " Node: " + actionedUponNodeRef + "\n" +
+ " Content: " + writer,
+ e);
+ }
+ else
+ {
+ logger.warn(
+ "Metadata embedding failed (turn on DEBUG for full error): \n" +
+ " Extracter: " + this + "\n" +
+ " Node: " + actionedUponNodeRef + "\n" +
+ " Content: " + writer + "\n" +
+ " Failure: " + e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ protected void addParameterDefinitions(List arg0)
+ {
+ // None!
+ }
+}
diff --git a/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java
index dc14a650a7..959524e234 100644
--- a/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java
+++ b/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java
@@ -42,6 +42,7 @@ import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
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.ContentWriter;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.repository.datatype.TypeConversionException;
@@ -92,7 +93,7 @@ import org.springframework.extensions.surf.util.ISO8601DateFormat;
* @author Jesper Steen Møller
* @author Derek Hulley
*/
-abstract public class AbstractMappingMetadataExtracter implements MetadataExtracter
+abstract public class AbstractMappingMetadataExtracter implements MetadataExtracter, MetadataEmbedder
{
public static final String NAMESPACE_PROPERTY_PREFIX = "namespace.prefix.";
private static final String ERR_TYPE_CONVERSION = "metadata.extraction.err.type_conversion";
@@ -105,11 +106,14 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
private boolean initialized;
private Set supportedMimetypes;
+ private Set supportedEmbedMimetypes;
private OverwritePolicy overwritePolicy;
private boolean failOnTypeConversion;
protected Set supportedDateFormats = new HashSet(0);
private Map> mapping;
+ private Map> embedMapping;
private boolean inheritDefaultMapping;
+ private boolean inheritDefaultEmbedMapping;
/**
* Default constructor. If this is called, then {@link #isSupported(String)} should
@@ -137,10 +141,24 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
overwritePolicy = OverwritePolicy.PRAGMATIC;
failOnTypeConversion = true;
mapping = null; // The default will be fetched
+ embedMapping = null;
inheritDefaultMapping = false; // Any overrides are complete
+ inheritDefaultEmbedMapping = false;
initialized = false;
}
+ /**
+ * Constructor that can be used when the list of supported extract and embed mimetypes is known up front.
+ *
+ * @param supportedMimetypes the set of mimetypes supported for extraction by default
+ * @param supportedEmbedMimetypes the set of mimetypes supported for embedding by default
+ */
+ protected AbstractMappingMetadataExtracter(Set supportedMimetypes, Set supportedEmbedMimetypes)
+ {
+ this(supportedMimetypes);
+ this.supportedEmbedMimetypes = supportedEmbedMimetypes;
+ }
+
/**
* Set the registry to register with. If this is not set, then the default
* initialization will not auto-register the extracter for general use. It
@@ -187,7 +205,18 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
this.supportedMimetypes.clear();
this.supportedMimetypes.addAll(supportedMimetypes);
}
-
+
+ /**
+ * Set the mimetypes that are supported for embedding.
+ *
+ * @param supportedEmbedMimetypes
+ */
+ public void setSupportedEmbedMimetypes(Collection supportedEmbedMimetypes)
+ {
+ this.supportedEmbedMimetypes.clear();
+ this.supportedEmbedMimetypes.addAll(supportedEmbedMimetypes);
+ }
+
/**
* {@inheritDoc}
*
@@ -197,7 +226,21 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
{
return supportedMimetypes.contains(sourceMimetype);
}
-
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see #setSupportedEmbedMimetypes(Collection)
+ */
+ public boolean isEmbeddingSupported(String sourceMimetype)
+ {
+ if (supportedEmbedMimetypes == null)
+ {
+ return false;
+ }
+ return supportedEmbedMimetypes.contains(sourceMimetype);
+ }
+
/**
* TODO - This doesn't appear to be used, so should be removed / deprecated / replaced
* @return Returns 1.0 if the mimetype is supported, otherwise 0.0
@@ -308,6 +351,23 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
this.inheritDefaultMapping = inheritDefaultMapping;
}
+ /**
+ * Set if the embed property mappings augment or override the mapping generically provided by the
+ * extracter implementation. The default is false, i.e. any mapping set completely
+ * replaces the {@link #getDefaultEmbedMapping() default mappings}.
+ *
+ * @param inheritDefaultEmbedMapping true to add the configured embed mapping
+ * to the list of default embed mappings.
+ *
+ * @see #getDefaultEmbedMapping()
+ * @see #setEmbedMapping(Map)
+ * @see #setEmbedMappingProperties(Properties)
+ */
+ public void setInheritDefaultEmbedMapping(boolean inheritDefaultEmbedMapping)
+ {
+ this.inheritDefaultEmbedMapping = inheritDefaultEmbedMapping;
+ }
+
/**
* Set the mapping from document metadata to system metadata. It is possible to direct
* an extracted document property to several system properties. The conversion between
@@ -321,6 +381,19 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
this.mapping = mapping;
}
+ /**
+ * Set the embed mapping from document metadata to system metadata. It is possible to direct
+ * an model properties to several content file metadata keys. The conversion between
+ * the model property types and the content file metadata keys types will be done by the
+ * {@link org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter default converter}.
+ *
+ * @param embedMapping an embed mapping from model properties to content file metadata keys
+ */
+ public void setEmbedMapping(Map> embedMapping)
+ {
+ this.embedMapping = embedMapping;
+ }
+
/**
* Set the properties that contain the mapping from document metadata to system metadata.
* This is an alternative to the {@link #setMapping(Map)} method. Any mappings already
@@ -346,7 +419,33 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
{
mapping = readMappingProperties(mappingProperties);
}
-
+
+ /**
+ * Set the properties that contain the embed mapping from model properties to content file metadata.
+ * This is an alternative to the {@link #setEmbedMapping(Map)} method. Any mappings already
+ * present will be cleared out.
+ *
+ * The property mapping is of the form:
+ *
+ * The embed mapping can therefore be from a model property onto several content file metadata properties.
+ *
+ * @param embedMappingProperties the properties that map model properties to content file metadata properties
+ */
+ public void setEmbedMappingProperties(Properties embedMappingProperties)
+ {
+ embedMapping = readEmbedMappingProperties(embedMappingProperties);
+ }
+
/**
* Helper method for derived classes to obtain the mappings that will be applied to raw
* values. This should be called after initialization in order to guarantee the complete
@@ -372,7 +471,29 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
}
return Collections.unmodifiableMap(mapping);
}
-
+
+ /**
+ * Helper method for derived classes to obtain the embed mappings.
+ * This should be called after initialization in order to guarantee the complete
+ * map is given.
+ *
+ * Normally, the list of properties that can be embedded in a document is fixed and
+ * well-known.. But some implementations may have
+ * an extra, indeterminate set of values available for embedding. If the embedding of
+ * these runtime parameters is expensive, then the keys provided by the return value can
+ * be used to embed values in the documents. The metadata embedding becomes fully
+ * configuration-driven, i.e. declaring further mappings will result in more values being
+ * embedded in the documents.
+ */
+ protected final Map> getEmbedMapping()
+ {
+ if (!initialized)
+ {
+ throw new UnsupportedOperationException("The complete embed mapping is only available after initialization.");
+ }
+ return Collections.unmodifiableMap(embedMapping);
+ }
+
/**
* A utility method to read mapping properties from a resource file and convert to the map form.
*
@@ -490,15 +611,137 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac
" Mapping: " + entry);
}
}
- if (logger.isDebugEnabled())
+ if (logger.isTraceEnabled())
{
- logger.debug("Added mapping from " + documentProperty + " to " + qnames);
+ logger.trace("Added mapping from " + documentProperty + " to " + qnames);
}
}
// Done
return convertedMapping;
}
-
+
+ /**
+ * A utility method to read embed mapping properties from a resource file and convert to the map form.
+ *
+ * @param propertiesUrl A standard Properties file URL location
+ *
+ * @see #setEmbedMappingProperties(Properties)
+ */
+ protected Map> readEmbedMappingProperties(String propertiesUrl)
+ {
+ InputStream is = null;
+ try
+ {
+ is = getClass().getClassLoader().getResourceAsStream(propertiesUrl);
+ if(is == null)
+ {
+ return null;
+ }
+ Properties props = new Properties();
+ props.load(is);
+ // Process it
+ Map> map = readEmbedMappingProperties(props);
+ // Done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Loaded embed mapping properties from resource: " + propertiesUrl);
+ }
+ return map;
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Unable to load properties file to read extracter embed mapping properties: \n" +
+ " Extracter: " + this + "\n" +
+ " Bundle: " + propertiesUrl,
+ e);
+ }
+ finally
+ {
+ if (is != null)
+ {
+ try { is.close(); } catch (Throwable e) {}
+ }
+ }
+ }
+
+ /**
+ * A utility method to convert mapping properties to the Map form.
+ *
+ * Different from readMappingProperties in that keys are the Alfresco QNames
+ * and values are file metadata properties.
+ *
+ * @see #setMappingProperties(Properties)
+ */
+ protected Map> readEmbedMappingProperties(Properties mappingProperties)
+ {
+ Map namespacesByPrefix = new HashMap(5);
+ // Get the namespaces
+ for (Map.Entry