From be1a9f5d172495f93209462997f016a8d570d2a2 Mon Sep 17 00:00:00 2001 From: Ray Gauss Date: Fri, 2 Nov 2012 11:53:52 +0000 Subject: [PATCH] Merged BRANCHES/DEV/RGAUSS/4.2-CORE-CHANGES-43298 to HEAD: 43309: Merged BRANCHES/DEV/RGAUSS/V4.1-BUG-FIX-TAG-MAPPING to BRANCHES/DEV/RGAUSS/4.2-CORE-CHANGES-43298: 39447: Merged BRANCHES/DEV/RGAUSS/V4.1-BUG-FIX-38527 to BRANCHES/DEV/RGAUSS/V4.1-BUG-FIX-TAG-MAPPING: 38719: ALF-14965: Ability to Map Extracted Metadata to Standard Tags - Added more specific MalformedNodeRefException - Changed NodeRef to throw MalformedNodeRefException on a bad string constructor rather than generic AlfrescoRunTimeException - ContentMetadataExtracter: Added enableStringTagging boolean field - ContentMetadataExtracter: Added taggingService - ContentMetadataExtracter: Added addTags method responsible for iterating the raw value from the metadata extracter and creating either string tags or NodeRef links - ContentMetadataExtracter: Added check for instanceof AbstractMappingMetadataExtracter and if so set its enableStringTagging field - ContentMetadataExtracter: Added check for enableStringTagging in executeImpl and if enabled call addTags - AbstractMappingMetadataExtracter: Added enableStringTagging boolean field - AbstractMappingMetadataExtracter: Added catch of MalformedNodeRefException and if string tagging enabled leave the raw properties for processing by ContentMetadataExtracter 39448: ALF-14965: Ability to Map Extracted Metadata to Standard Tags - Added fix for single valued raw properties - Added tag mapping unit test and test resource 39449: ALF-14965: Ability to Map Extracted Metadata to Standard Tags - Added better class javadoc 39479: ALF-14965: Ability to Map Extracted Metadata to Standard Tags - Changed behavior of addition of tags by NodeRef - Changed where some items were setup in the unit test - Added manual test keywords to those extracted from file in unit test - Added testing of addition of tag by NodeRef 43324: ALF-14965: Ability to Map Extracted Metadata to Standard Tags - Added Javadoc to AbstractMappingMetadataExtracter.setEnableStringTagging - Changed check of enableStringTagging in AbstractMappingMetadataExtracter.convertSystemPropertyValues to allow graceful failure if mappings to cm:taggable are present but enableStringTagging is false git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@43335 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../executer/ContentMetadataExtracter.java | 125 +++++- ...ontentMetadataExtracterTagMappingTest.java | 385 ++++++++++++++++++ .../AbstractMappingMetadataExtracter.java | 47 +++ source/test-resources/quick/quickIPTC.jpg | Bin 0 -> 24548 bytes 4 files changed, 552 insertions(+), 5 deletions(-) create mode 100644 source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTagMappingTest.java create mode 100644 source/test-resources/quick/quickIPTC.jpg diff --git a/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java index 023501108b..615f8dc9dd 100644 --- a/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java +++ b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracter.java @@ -19,6 +19,8 @@ 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; @@ -26,6 +28,7 @@ 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; @@ -37,6 +40,8 @@ import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; 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; @@ -60,8 +65,10 @@ public class ContentMetadataExtracter extends ActionExecuterAbstractBase 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() { @@ -91,6 +98,14 @@ public class ContentMetadataExtracter extends ActionExecuterAbstractBase this.dictionaryService = dictService; } + /** + * @param taggingService The TaggingService to set. + */ + public void setTaggingService(TaggingService taggingService) + { + this.taggingService = taggingService; + } + /** * @param metadataExtracterRegistry The metadataExtracterRegistry to set. */ @@ -110,7 +125,92 @@ public class ContentMetadataExtracter extends ActionExecuterAbstractBase { 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); + 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()); + } + tags.add(tagName); + } + 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) @@ -144,6 +244,10 @@ public class ContentMetadataExtracter extends ActionExecuterAbstractBase // 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); @@ -212,11 +316,22 @@ public class ContentMetadataExtracter extends ActionExecuterAbstractBase ClassDefinition propertyContainerDef = propertyDef.getContainerClass(); if (propertyContainerDef.isAspect()) { - QName aspectQName = propertyContainerDef.getName(); - requiredAspectQNames.add(aspectQName); - // Get all properties associated with the aspect - Set aspectProperties = propertyContainerDef.getProperties().keySet(); - aspectPropertyQNames.addAll(aspectProperties); + 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); + } } } diff --git a/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTagMappingTest.java b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTagMappingTest.java new file mode 100644 index 0000000000..7e86ba72b4 --- /dev/null +++ b/source/java/org/alfresco/repo/action/executer/ContentMetadataExtracterTagMappingTest.java @@ -0,0 +1,385 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.ActionModel; +import org.alfresco.repo.action.AsynchronousActionExecutionQueuePolicies; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.metadata.MetadataExtracterRegistry; +import org.alfresco.repo.content.metadata.TikaPoweredMetadataExtracter; +import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.tagging.TaggingServiceImplTest; +import org.alfresco.repo.tagging.TaggingServiceImplTest.AsyncOccurs; +import org.alfresco.repo.tagging.UpdateTagScopesActionExecuter; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.audit.AuditService; +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.cmr.repository.StoreRef; +import org.alfresco.service.cmr.tagging.TaggingService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.parser.Parser; +import org.apache.tika.parser.jpeg.JpegParser; +import org.springframework.context.ConfigurableApplicationContext; + +import com.google.common.collect.Sets; + +/** + * Test of the ActionExecuter for extracting metadata, specifically for + * the mapping to cm:taggable tags which requires different transaction + * mechanisms than the existing {@link ContentMetadataExtracterTest}. + * + * @author Roy Wetherall + * @author Nick Burch + * @author Ray Gauss II + */ +public class ContentMetadataExtracterTagMappingTest extends TestCase +{ + private static ConfigurableApplicationContext ctx = + (ConfigurableApplicationContext)ApplicationContextHelper.getApplicationContext(); + + protected static final String TAGGING_AUDIT_APPLICATION_NAME = "Alfresco Tagging Service"; + protected static final String QUICK_FILENAME = "quickIPTC.jpg"; + protected static final String QUICK_KEYWORD = "fox"; + protected static final String TAG_1 = "tag one"; + protected static final String TAG_2 = "tag two"; + protected static final String TAG_3 = "Tag Three"; + + /** Services */ + private TaggingService taggingService; + private NodeService nodeService; + private ContentService contentService; + private AuditService auditService; + private TransactionService transactionService; + private AuthenticationComponent authenticationComponent; + private AsyncOccurs asyncOccurs; + + private static StoreRef storeRef; + private static NodeRef rootNode; + private NodeRef folder; + private NodeRef document; + + private ContentMetadataExtracter executer; + private TagMappingMetadataExtracter extractor; + + private static boolean init = false; + + private final static String ID = GUID.generate(); + + + @Override + protected void setUp() throws Exception + { + // Detect any dangling transactions as there is a lot of direct UserTransaction manipulation + if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE) + { + throw new IllegalStateException( + "There should not be any transactions when starting test: " + + AlfrescoTransactionSupport.getTransactionId() + " started at " + + new Date(AlfrescoTransactionSupport.getTransactionStartTime())); + } + + // Get services + this.taggingService = (TaggingService)ctx.getBean("TaggingService"); + this.nodeService = (NodeService) ctx.getBean("NodeService"); + this.contentService = (ContentService) ctx.getBean("ContentService"); + + this.transactionService = (TransactionService)ctx.getBean("transactionComponent"); + this.auditService = (AuditService)ctx.getBean("auditService"); + this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent"); + + this.executer = (ContentMetadataExtracter) ctx.getBean("extract-metadata"); + executer.setEnableStringTagging(true); + executer.setTaggingService(taggingService); + + if (init == false) + { + this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ + + @Override + public Void execute() throws Throwable + { + // Authenticate as the system user + authenticationComponent.setSystemUserAsCurrentUser(); + + // Create the store and get the root node + ContentMetadataExtracterTagMappingTest.storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + ContentMetadataExtracterTagMappingTest.rootNode = nodeService.getRootNode(ContentMetadataExtracterTagMappingTest.storeRef); + + // Create the required tagging category + NodeRef catContainer = nodeService.createNode(ContentMetadataExtracterTagMappingTest.rootNode, ContentModel.ASSOC_CHILDREN, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categoryContainer"), ContentModel.TYPE_CONTAINER).getChildRef(); + NodeRef catRoot = nodeService.createNode( + catContainer, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "categoryRoot"), + ContentModel.TYPE_CATEGORYROOT).getChildRef(); + nodeService.createNode( + catRoot, + ContentModel.ASSOC_CATEGORIES, + ContentModel.ASPECT_TAGGABLE, + ContentModel.TYPE_CATEGORY).getChildRef(); + + MetadataExtracterRegistry registry = (MetadataExtracterRegistry) ctx.getBean("metadataExtracterRegistry"); + extractor = new TagMappingMetadataExtracter(); + extractor.setRegistry(registry); + extractor.register(); + + init = true; + return null; + }}); + + } + + // We want to know when tagging actions have finished running + asyncOccurs = (new TaggingServiceImplTest()).new AsyncOccurs(); + ((PolicyComponent)ctx.getBean("policyComponent")).bindClassBehaviour( + AsynchronousActionExecutionQueuePolicies.OnAsyncActionExecute.QNAME, + ActionModel.TYPE_ACTION, + new JavaBehaviour(asyncOccurs, "onAsyncActionExecute", NotificationFrequency.EVERY_EVENT) + ); + + // We do want action tracking whenever the tag scope updater runs + UpdateTagScopesActionExecuter updateTagsAction = + (UpdateTagScopesActionExecuter)ctx.getBean("update-tagscope"); + updateTagsAction.setTrackStatus(true); + + // Create the folders and documents to be tagged + createTestDocumentsAndFolders(); + } + + @Override + protected void tearDown() throws Exception + { + removeTestDocumentsAndFolders(); + if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE) + { + fail("Test is not transaction-safe. Fix up transaction handling and re-test."); + } + } + + private void createTestDocumentsAndFolders() throws Exception + { + this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ + + @Override + public Void execute() throws Throwable + { + + // Authenticate as the system user + authenticationComponent.setSystemUserAsCurrentUser(); + + String guid = GUID.generate(); + + // Create a folder + Map folderProps = new HashMap(1); + folderProps.put(ContentModel.PROP_NAME, "testFolder" + guid); + folder = nodeService.createNode( + ContentMetadataExtracterTagMappingTest.rootNode, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "testFolder" + guid), + ContentModel.TYPE_FOLDER, + folderProps).getChildRef(); + + // Create a node + Map docProps = new HashMap(1); + docProps.put(ContentModel.PROP_NAME, "testDocument" + guid + ".jpg"); + document = nodeService.createNode( + folder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "testDocument" + guid + ".jpg"), + ContentModel.TYPE_CONTENT, + docProps).getChildRef(); + + try + { + ContentWriter cw = contentService.getWriter(document, ContentModel.PROP_CONTENT, true); + cw.setMimetype(MimetypeMap.MIMETYPE_IMAGE_JPEG); + cw.putContent(AbstractContentTransformerTest.loadNamedQuickTestFile(QUICK_FILENAME)); + } + catch (Exception e) + { + fail(e.getMessage()); + } + + return null; + } + }); + } + + private void removeTestDocumentsAndFolders() throws Exception + { + this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ + @Override + public Void execute() throws Throwable + { + // Authenticate as the system user + authenticationComponent.setSystemUserAsCurrentUser(); + + // If anything is a tag scope, stop it being + NodeRef[] nodes = new NodeRef[] { document, folder }; + for(NodeRef nodeRef : nodes) + { + if(taggingService.isTagScope(nodeRef)) + { + taggingService.removeTagScope(nodeRef); + } + } + + // Remove the sample nodes + for(NodeRef nodeRef : nodes) + { + nodeService.deleteNode(nodeRef); + } + + // Tidy up the audit component, now all the nodes have gone + auditService.clearAudit( + TAGGING_AUDIT_APPLICATION_NAME, + 0l, System.currentTimeMillis()+1 + ); + return null; + } + }); + } + + private static class TagMappingMetadataExtracter extends TikaPoweredMetadataExtracter + { + + private String existingTagNodeRef; + + public TagMappingMetadataExtracter() + { + super(Sets.newHashSet(MimetypeMap.MIMETYPE_IMAGE_JPEG)); + Properties mappingProperties = new Properties(); + // TODO move to new keyword once tika is upgraded + mappingProperties.put(Metadata.KEYWORDS, ContentModel.PROP_TAGS.toString()); + mappingProperties.put(Metadata.DESCRIPTION, ContentModel.PROP_DESCRIPTION.toString()); + setMappingProperties(mappingProperties); + } + + public void setExistingTagNodeRef(String existingTagNodeRef) + { + this.existingTagNodeRef = existingTagNodeRef; + } + + @Override + protected Map> getDefaultMapping() + { + // No need to give anything back as we have explicitly set the mapping already + return new HashMap>(0); + } + @Override + public boolean isSupported(String sourceMimetype) + { + return sourceMimetype.equals(MimetypeMap.MIMETYPE_IMAGE_JPEG); + } + + @Override + protected Parser getParser() + { + return new JpegParser(); + } + + @SuppressWarnings("unchecked") + public Map extractRaw(ContentReader reader) throws Throwable + { + Map rawMap = super.extractRaw(reader); + + // Add some test keywords to those actually extracted from the file including a nodeRef + List keywords = new ArrayList(Arrays.asList(new String[] { existingTagNodeRef, TAG_2, TAG_3 })); + Serializable extractedKeywords = rawMap.get(Metadata.KEYWORDS); + if (extractedKeywords != null && extractedKeywords instanceof String) + { + keywords.add((String) extractedKeywords); + } + else if (extractedKeywords != null && extractedKeywords instanceof Collection) + { + keywords.addAll((Collection) extractedKeywords); + } + putRawValue(Metadata.KEYWORDS, (Serializable) keywords, rawMap); + return rawMap; + } + } + + /** + * Test execution of mapping strings to tags + */ + public void testTagMapping() + { + this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback(){ + + @Override + public Void execute() throws Throwable + { + NodeRef existingTagNodeRef = taggingService.createTag(storeRef, TAG_1); + extractor.setExistingTagNodeRef(existingTagNodeRef.toString()); + + ActionImpl action = new ActionImpl(document, ID, ContentMetadataExtracter.EXECUTOR_NAME, null); + executer.execute(action, document); + + // Test extracted properties + assertEquals(ContentMetadataExtracterTest.QUICK_DESCRIPTION, + nodeService.getProperty(document, ContentModel.PROP_DESCRIPTION)); + assertTrue("storeRef tags should contain '" + QUICK_KEYWORD + "'", + taggingService.getTags(storeRef).contains(QUICK_KEYWORD)); + assertTrue("document's tags should contain '" + QUICK_KEYWORD + "'", + taggingService.getTags(document).contains(QUICK_KEYWORD)); + + // Test manually added keyword + assertTrue("tags should contain '" + TAG_2 + "'", + taggingService.getTags(document).contains(TAG_2)); + + // Test manually added nodeRef keyword + assertTrue("tags should contain '" + TAG_1 + "'", + taggingService.getTags(document).contains(TAG_1)); + + return null; + } + }); + } + +} diff --git a/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java index 959524e234..e829e19e9f 100644 --- a/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java +++ b/source/java/org/alfresco/repo/content/metadata/AbstractMappingMetadataExtracter.java @@ -38,11 +38,13 @@ import java.util.Set; import java.util.StringTokenizer; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; 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.MalformedNodeRefException; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.repository.datatype.TypeConversionException; @@ -114,6 +116,7 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac private Map> embedMapping; private boolean inheritDefaultMapping; private boolean inheritDefaultEmbedMapping; + private boolean enableStringTagging; /** * Default constructor. If this is called, then {@link #isSupported(String)} should @@ -351,6 +354,18 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac this.inheritDefaultMapping = inheritDefaultMapping; } + /** + * Whether or not to enable the pass through 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; + } + /** * 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 @@ -1298,6 +1313,38 @@ abstract public class AbstractMappingMetadataExtracter implements MetadataExtrac propertyValue); } } + catch (MalformedNodeRefException e) + { + if (propertyQName.equals(ContentModel.PROP_TAGS)) + { + if (enableStringTagging) + { + // We must want to map tag string values instead of nodeRefs + // ContentMetadataExtracter will take care of tagging by string + ArrayList list = new ArrayList(1); + for (Object value : (Object[]) propertyValue) + { + list.add(value); + } + convertedProperties.put(propertyQName, list); + } + else + { + if (logger.isInfoEnabled()) + { + logger.info("enableStringTagging is false and could not convert " + + propertyQName.toString() + ": " + e.getMessage()); + } + } + } + else + { + if (failOnTypeConversion) + { + throw e; + } + } + } } // Done return convertedProperties; diff --git a/source/test-resources/quick/quickIPTC.jpg b/source/test-resources/quick/quickIPTC.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8711d8f86cd74caa22a806ea089b85d26e38bb45 GIT binary patch literal 24548 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 literal 0 HcmV?d00001