001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * vpasquier <vpasquier@nuxeo.com> 016 */ 017package org.nuxeo.binary.metadata.internals; 018 019import java.io.Serializable; 020import java.util.ArrayList; 021import java.util.Date; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.commons.lang.ObjectUtils; 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032import org.jboss.el.ExpressionFactoryImpl; 033import org.nuxeo.binary.metadata.api.BinaryMetadataConstants; 034import org.nuxeo.binary.metadata.api.BinaryMetadataException; 035import org.nuxeo.binary.metadata.api.BinaryMetadataProcessor; 036import org.nuxeo.binary.metadata.api.BinaryMetadataService; 037import org.nuxeo.ecm.core.api.Blob; 038import org.nuxeo.ecm.core.api.CoreSession; 039import org.nuxeo.ecm.core.api.DocumentModel; 040import org.nuxeo.ecm.core.api.model.Property; 041import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 042import org.nuxeo.ecm.platform.actions.ActionContext; 043import org.nuxeo.ecm.platform.actions.ELActionContext; 044import org.nuxeo.ecm.platform.actions.ejb.ActionManager; 045import org.nuxeo.ecm.platform.el.ExpressionContext; 046import org.nuxeo.runtime.api.Framework; 047 048import com.google.common.base.Predicate; 049import com.google.common.collect.Sets; 050 051/** 052 * @since 7.1 053 */ 054public class BinaryMetadataServiceImpl implements BinaryMetadataService { 055 056 private static final Log log = LogFactory.getLog(BinaryMetadataServiceImpl.class); 057 058 @Override 059 public Map<String, Object> readMetadata(String processorName, Blob blob, List<String> metadataNames, 060 boolean ignorePrefix) { 061 try { 062 BinaryMetadataProcessor processor = getProcessor(processorName); 063 return processor.readMetadata(blob, metadataNames, ignorePrefix); 064 } catch (NoSuchMethodException e) { 065 throw new BinaryMetadataException(e); 066 } 067 } 068 069 @Override 070 public Map<String, Object> readMetadata(Blob blob, List<String> 071 metadataNames, boolean ignorePrefix) { 072 try { 073 BinaryMetadataProcessor processor = getProcessor(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID); 074 return processor.readMetadata(blob, metadataNames, ignorePrefix); 075 } catch (NoSuchMethodException e) { 076 throw new BinaryMetadataException(e); 077 } 078 } 079 080 @Override 081 public Map<String, Object> readMetadata(Blob blob, boolean ignorePrefix) { 082 try { 083 BinaryMetadataProcessor processor = getProcessor(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID); 084 return processor.readMetadata(blob, ignorePrefix); 085 } catch (NoSuchMethodException e) { 086 throw new BinaryMetadataException(e); 087 } 088 } 089 090 @Override 091 public Map<String, Object> readMetadata(String processorName, Blob blob, boolean ignorePrefix) { 092 try { 093 BinaryMetadataProcessor processor = getProcessor(processorName); 094 return processor.readMetadata(blob, ignorePrefix); 095 } catch (NoSuchMethodException e) { 096 throw new BinaryMetadataException(e); 097 } 098 } 099 100 @Override 101 public Blob writeMetadata(String processorName, Blob blob, Map<String, String> metadata, boolean ignorePrefix) { 102 try { 103 BinaryMetadataProcessor processor = getProcessor(processorName); 104 return processor.writeMetadata(blob, metadata, ignorePrefix); 105 } catch (NoSuchMethodException e) { 106 throw new BinaryMetadataException(e); 107 } 108 } 109 110 @Override 111 public Blob writeMetadata(Blob blob, Map<String, String> metadata, boolean ignorePrefix) { 112 try { 113 BinaryMetadataProcessor processor = getProcessor(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID); 114 return processor.writeMetadata(blob, metadata, ignorePrefix); 115 } catch (NoSuchMethodException e) { 116 throw new BinaryMetadataException(e); 117 } 118 } 119 120 @Override 121 public Blob writeMetadata(String processorName, Blob blob, String mappingDescriptorId, DocumentModel doc) { 122 try { 123 // Creating mapping properties Map. 124 Map<String, String> metadataMapping = new HashMap<>(); 125 MetadataMappingDescriptor mappingDescriptor = BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().get( 126 mappingDescriptorId); 127 for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) { 128 Serializable value = doc.getPropertyValue(metadataDescriptor.getXpath()); 129 metadataMapping.put(metadataDescriptor.getName(), ObjectUtils.toString(value)); 130 } 131 BinaryMetadataProcessor processor = getProcessor(processorName); 132 return processor.writeMetadata(blob, metadataMapping, mappingDescriptor.getIgnorePrefix()); 133 } catch (NoSuchMethodException e) { 134 throw new BinaryMetadataException(e); 135 } 136 137 } 138 139 @Override 140 public Blob writeMetadata(Blob blob, String mappingDescriptorId, DocumentModel doc) { 141 try { 142 // Creating mapping properties Map. 143 Map<String, String> metadataMapping = new HashMap<>(); 144 MetadataMappingDescriptor mappingDescriptor = BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().get( 145 mappingDescriptorId); 146 for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) { 147 Serializable value = doc.getPropertyValue(metadataDescriptor.getXpath()); 148 metadataMapping.put(metadataDescriptor.getName(), ObjectUtils.toString(value)); 149 } 150 BinaryMetadataProcessor processor = getProcessor(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID); 151 return processor.writeMetadata(blob, metadataMapping, mappingDescriptor.getIgnorePrefix()); 152 } catch (NoSuchMethodException e) { 153 throw new BinaryMetadataException(e); 154 } 155 } 156 157 @Override 158 public void writeMetadata(DocumentModel doc, CoreSession session) { 159 // Check if rules applying for this document. 160 ActionContext actionContext = createActionContext(doc); 161 Set<MetadataRuleDescriptor> ruleDescriptors = checkFilter(actionContext); 162 List<String> mappingDescriptorIds = new ArrayList<>(); 163 for (MetadataRuleDescriptor ruleDescriptor : ruleDescriptors) { 164 mappingDescriptorIds.addAll(ruleDescriptor.getMetadataMappingIdDescriptors()); 165 } 166 if (mappingDescriptorIds.isEmpty()) { 167 return; 168 } 169 170 // For each mapping descriptors, overriding mapping document properties. 171 for (String mappingDescriptorId : mappingDescriptorIds) { 172 if (!BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().containsKey(mappingDescriptorId)) { 173 log.warn("Missing binary metadata descriptor with id '" + mappingDescriptorId 174 + "'. Or check your rule contribution with proper metadataMapping-id."); 175 continue; 176 } 177 writeMetadata(doc, session, mappingDescriptorId); 178 } 179 } 180 181 @Override 182 public void writeMetadata(DocumentModel doc, CoreSession session, String mappingDescriptorId) { 183 // Creating mapping properties Map. 184 Map<String, String> metadataMapping = new HashMap<>(); 185 List<String> blobMetadata = new ArrayList<>(); 186 MetadataMappingDescriptor mappingDescriptor = BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().get( 187 mappingDescriptorId); 188 boolean ignorePrefix = mappingDescriptor.getIgnorePrefix(); 189 // Extract blob from the contributed xpath 190 Blob blob = doc.getProperty(mappingDescriptor.getBlobXPath()).getValue(Blob.class); 191 if (blob != null && mappingDescriptor.getMetadataDescriptors() != null 192 && !mappingDescriptor.getMetadataDescriptors().isEmpty()) { 193 for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) { 194 metadataMapping.put(metadataDescriptor.getName(), metadataDescriptor.getXpath()); 195 blobMetadata.add(metadataDescriptor.getName()); 196 } 197 198 // Extract metadata from binary. 199 String processorId = mappingDescriptor.getProcessor(); 200 Map<String, Object> blobMetadataOutput; 201 if (processorId != null) { 202 blobMetadataOutput = readMetadata(processorId, blob, blobMetadata, ignorePrefix); 203 204 } else { 205 blobMetadataOutput = readMetadata(blob, blobMetadata, ignorePrefix); 206 } 207 208 // Write doc properties from outputs. 209 for (String metadata : blobMetadataOutput.keySet()) { 210 Object metadataValue = blobMetadataOutput.get(metadata); 211 if (!(metadataValue instanceof Date)) { 212 metadataValue = metadataValue.toString(); 213 } 214 doc.setPropertyValue(metadataMapping.get(metadata), (Serializable) metadataValue); 215 } 216 217 // document should exist if id != null 218 if (doc.getId() != null && session.exists(doc.getRef())) { 219 session.saveDocument(doc); 220 } 221 } 222 } 223 224 /*--------------------- Event Service --------------------------*/ 225 226 @Override 227 public void handleSyncUpdate(DocumentModel doc, DocumentEventContext docCtx) { 228 LinkedList<MetadataMappingDescriptor> syncMappingDescriptors = getSyncMapping(doc, docCtx); 229 if (syncMappingDescriptors != null) { 230 handleUpdate(syncMappingDescriptors, doc, docCtx); 231 } 232 } 233 234 @Override 235 public void handleUpdate(List<MetadataMappingDescriptor> mappingDescriptors, DocumentModel doc, 236 DocumentEventContext docCtx) { 237 for (MetadataMappingDescriptor mappingDescriptor : mappingDescriptors) { 238 Property fileProp = doc.getProperty(mappingDescriptor.getBlobXPath()); 239 boolean isDirtyMapping = isDirtyMapping(mappingDescriptor, doc); 240 Blob blob = fileProp.getValue(Blob.class); 241 if (blob != null) { 242 if (fileProp.isDirty()) { 243 if (isDirtyMapping) { 244 // if Blob dirty and document metadata dirty, write metadata from doc to Blob 245 Blob newBlob = writeMetadata(fileProp.getValue(Blob.class), mappingDescriptor.getId(), doc); 246 fileProp.setValue(newBlob); 247 } else { 248 // if Blob dirty and document metadata not dirty, write metadata from Blob to doc 249 writeMetadata(doc, docCtx.getCoreSession()); 250 } 251 } else { 252 if (isDirtyMapping) { 253 // if Blob not dirty and document metadata dirty, write metadata from doc to Blob 254 Blob newBlob = writeMetadata(fileProp.getValue(Blob.class), mappingDescriptor.getId(), doc); 255 fileProp.setValue(newBlob); 256 } 257 } 258 } 259 } 260 } 261 262 /*--------------------- Utils --------------------------*/ 263 264 /** 265 * Check for each Binary Rule if the document is accepted or not. 266 * 267 * @return the list of metadata which should be processed sorted by rules order. (high to low priority) 268 */ 269 protected Set<MetadataRuleDescriptor> checkFilter(final ActionContext actionContext) { 270 final ActionManager actionService = Framework.getLocalService(ActionManager.class); 271 Set<MetadataRuleDescriptor> filtered = Sets.filter(BinaryMetadataComponent.self.ruleRegistry.contribs, 272 new Predicate<MetadataRuleDescriptor>() { 273 274 @Override 275 public boolean apply(MetadataRuleDescriptor input) { 276 if (!input.getEnabled()) { 277 return false; 278 } 279 for (String filterId : input.getFilterIds()) { 280 if (!actionService.checkFilter(filterId, actionContext)) { 281 return false; 282 } 283 } 284 return true; 285 } 286 287 }); 288 return filtered; 289 } 290 291 protected ActionContext createActionContext(DocumentModel doc) { 292 ActionContext actionContext = new ELActionContext(new ExpressionContext(), new ExpressionFactoryImpl()); 293 actionContext.setCurrentDocument(doc); 294 return actionContext; 295 } 296 297 protected BinaryMetadataProcessor getProcessor(String processorId) throws NoSuchMethodException { 298 return BinaryMetadataComponent.self.processorRegistry.getProcessor(processorId); 299 } 300 301 /** 302 * @return Dirty metadata from metadata mapping contribution and handle async processes. 303 */ 304 public LinkedList<MetadataMappingDescriptor> getSyncMapping(DocumentModel doc, DocumentEventContext docCtx) { 305 // Check if rules applying for this document. 306 ActionContext actionContext = createActionContext(doc); 307 Set<MetadataRuleDescriptor> ruleDescriptors = checkFilter(actionContext); 308 Set<String> syncMappingDescriptorIds = new HashSet<>(); 309 HashSet<String> asyncMappingDescriptorIds = new HashSet<>(); 310 for (MetadataRuleDescriptor ruleDescriptor : ruleDescriptors) { 311 if (ruleDescriptor.getIsAsync()) { 312 asyncMappingDescriptorIds.addAll(ruleDescriptor.getMetadataMappingIdDescriptors()); 313 continue; 314 } 315 syncMappingDescriptorIds.addAll(ruleDescriptor.getMetadataMappingIdDescriptors()); 316 } 317 318 // Handle async rules which should be taken into account in async listener. 319 if (!asyncMappingDescriptorIds.isEmpty()) { 320 docCtx.setProperty(BinaryMetadataConstants.ASYNC_MAPPING_RESULT, getMapping(asyncMappingDescriptorIds)); 321 docCtx.setProperty(BinaryMetadataConstants.ASYNC_BINARY_METADATA_EXECUTE, Boolean.TRUE); 322 } 323 324 if (syncMappingDescriptorIds.isEmpty()) { 325 return null; 326 } 327 return getMapping(syncMappingDescriptorIds); 328 } 329 330 protected LinkedList<MetadataMappingDescriptor> getMapping(Set<String> mappingDescriptorIds) { 331 // For each mapping descriptors, store mapping. 332 LinkedList<MetadataMappingDescriptor> mappingResult = new LinkedList<>(); 333 for (String mappingDescriptorId : mappingDescriptorIds) { 334 if (!BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().containsKey(mappingDescriptorId)) { 335 log.warn("Missing binary metadata descriptor with id '" + mappingDescriptorId 336 + "'. Or check your rule contribution with proper metadataMapping-id."); 337 continue; 338 } 339 mappingResult.add(BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().get( 340 mappingDescriptorId)); 341 } 342 return mappingResult; 343 } 344 345 /** 346 * Maps inspector only. 347 */ 348 protected boolean isDirtyMapping(MetadataMappingDescriptor mappingDescriptor, DocumentModel doc) { 349 Map<String, String> mappingResult = new HashMap<>(); 350 for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) { 351 mappingResult.put(metadataDescriptor.getXpath(), metadataDescriptor.getName()); 352 } 353 // Returning only dirty properties 354 HashMap<String, Object> resultDirtyMapping = new HashMap<>(); 355 for (String metadata : mappingResult.keySet()) { 356 Property property = doc.getProperty(metadata); 357 if (property.isDirty()) { 358 resultDirtyMapping.put(mappingResult.get(metadata), doc.getPropertyValue(metadata)); 359 } 360 } 361 return !resultDirtyMapping.isEmpty(); 362 } 363}