001/* 002 * (C) Copyright 2014-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * vpasquier <[email protected]> 018 * ajusto <[email protected]> 019 */ 020package org.nuxeo.binary.metadata.internals; 021 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Date; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032import java.util.stream.Collectors; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.binary.metadata.api.BinaryMetadataConstants; 037import org.nuxeo.binary.metadata.api.BinaryMetadataException; 038import org.nuxeo.binary.metadata.api.BinaryMetadataProcessor; 039import org.nuxeo.binary.metadata.api.BinaryMetadataService; 040import org.nuxeo.ecm.core.api.Blob; 041import org.nuxeo.ecm.core.api.DocumentModel; 042import org.nuxeo.ecm.core.api.PropertyException; 043import org.nuxeo.ecm.core.api.model.Property; 044import org.nuxeo.ecm.core.blob.BlobManager; 045import org.nuxeo.ecm.core.blob.BlobProvider; 046import org.nuxeo.ecm.platform.actions.ActionContext; 047import org.nuxeo.ecm.platform.actions.ELActionContext; 048import org.nuxeo.ecm.platform.actions.ejb.ActionManager; 049import org.nuxeo.runtime.api.Framework; 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 protected BinaryMetadataComponent binaryMetadataComponent; 059 060 protected BinaryMetadataServiceImpl(BinaryMetadataComponent binaryMetadataComponent) { 061 this.binaryMetadataComponent = binaryMetadataComponent; 062 } 063 064 @Override 065 public Map<String, Object> readMetadata(String processorName, Blob blob, List<String> metadataNames, 066 boolean ignorePrefix) { 067 try { 068 BinaryMetadataProcessor processor = getProcessor(processorName); 069 return processor.readMetadata(blob, metadataNames, ignorePrefix); 070 } catch (NoSuchMethodException e) { 071 throw new BinaryMetadataException(e); 072 } 073 } 074 075 @Override 076 public Map<String, Object> readMetadata(Blob blob, List<String> 077 metadataNames, boolean ignorePrefix) { 078 try { 079 BinaryMetadataProcessor processor = getProcessor(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID); 080 return processor.readMetadata(blob, metadataNames, ignorePrefix); 081 } catch (NoSuchMethodException e) { 082 throw new BinaryMetadataException(e); 083 } 084 } 085 086 @Override 087 public Map<String, Object> readMetadata(Blob blob, boolean ignorePrefix) { 088 try { 089 BinaryMetadataProcessor processor = getProcessor(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID); 090 return processor.readMetadata(blob, ignorePrefix); 091 } catch (NoSuchMethodException e) { 092 throw new BinaryMetadataException(e); 093 } 094 } 095 096 @Override 097 public Map<String, Object> readMetadata(String processorName, Blob blob, boolean ignorePrefix) { 098 try { 099 BinaryMetadataProcessor processor = getProcessor(processorName); 100 return processor.readMetadata(blob, ignorePrefix); 101 } catch (NoSuchMethodException e) { 102 throw new BinaryMetadataException(e); 103 } 104 } 105 106 @Override 107 public Blob writeMetadata(String processorName, Blob blob, Map<String, Object> metadata, boolean ignorePrefix) { 108 try { 109 BinaryMetadataProcessor processor = getProcessor(processorName); 110 return processor.writeMetadata(blob, metadata, ignorePrefix); 111 } catch (NoSuchMethodException e) { 112 throw new BinaryMetadataException(e); 113 } 114 } 115 116 @Override 117 public Blob writeMetadata(Blob blob, Map<String, Object> metadata, boolean ignorePrefix) { 118 try { 119 BinaryMetadataProcessor processor = getProcessor(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID); 120 return processor.writeMetadata(blob, metadata, ignorePrefix); 121 } catch (NoSuchMethodException e) { 122 throw new BinaryMetadataException(e); 123 } 124 } 125 126 @Override 127 public Blob writeMetadata(String processorName, Blob blob, String mappingDescriptorId, DocumentModel doc) { 128 try { 129 // Creating mapping properties Map. 130 Map<String, Object> metadataMapping = new HashMap<>(); 131 MetadataMappingDescriptor mappingDescriptor = binaryMetadataComponent.mappingRegistry.getMappingDescriptorMap().get( 132 mappingDescriptorId); 133 for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) { 134 metadataMapping.put(metadataDescriptor.getName(), doc.getPropertyValue(metadataDescriptor.getXpath())); 135 } 136 BinaryMetadataProcessor processor = getProcessor(processorName); 137 return processor.writeMetadata(blob, metadataMapping, mappingDescriptor.getIgnorePrefix()); 138 } catch (NoSuchMethodException e) { 139 throw new BinaryMetadataException(e); 140 } 141 142 } 143 144 @Override 145 public Blob writeMetadata(Blob blob, String mappingDescriptorId, DocumentModel doc) { 146 return writeMetadata(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID, blob, mappingDescriptorId, doc); 147 } 148 149 @Override 150 public void writeMetadata(DocumentModel doc) { 151 // Check if rules applying for this document. 152 ActionContext actionContext = createActionContext(doc); 153 Set<MetadataRuleDescriptor> ruleDescriptors = checkFilter(actionContext); 154 List<String> mappingDescriptorIds = new ArrayList<>(); 155 for (MetadataRuleDescriptor ruleDescriptor : ruleDescriptors) { 156 mappingDescriptorIds.addAll(ruleDescriptor.getMetadataMappingIdDescriptors()); 157 } 158 if (mappingDescriptorIds.isEmpty()) { 159 return; 160 } 161 162 // For each mapping descriptors, overriding mapping document properties. 163 for (String mappingDescriptorId : mappingDescriptorIds) { 164 if (!binaryMetadataComponent.mappingRegistry.getMappingDescriptorMap().containsKey(mappingDescriptorId)) { 165 log.warn("Missing binary metadata descriptor with id '" + mappingDescriptorId 166 + "'. Or check your rule contribution with proper metadataMapping-id."); 167 continue; 168 } 169 writeMetadata(doc, mappingDescriptorId); 170 } 171 } 172 173 @Override 174 public void writeMetadata(DocumentModel doc, String mappingDescriptorId) { 175 // Creating mapping properties Map. 176 Map<String, String> metadataMapping = new HashMap<>(); 177 List<String> blobMetadata = new ArrayList<>(); 178 MetadataMappingDescriptor mappingDescriptor = binaryMetadataComponent.mappingRegistry.getMappingDescriptorMap().get( 179 mappingDescriptorId); 180 boolean ignorePrefix = mappingDescriptor.getIgnorePrefix(); 181 // Extract blob from the contributed xpath 182 Blob blob = doc.getProperty(mappingDescriptor.getBlobXPath()).getValue(Blob.class); 183 if (blob != null && mappingDescriptor.getMetadataDescriptors() != null 184 && !mappingDescriptor.getMetadataDescriptors().isEmpty()) { 185 for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) { 186 metadataMapping.put(metadataDescriptor.getName(), metadataDescriptor.getXpath()); 187 blobMetadata.add(metadataDescriptor.getName()); 188 } 189 190 // Extract metadata from binary. 191 String processorId = mappingDescriptor.getProcessor(); 192 Map<String, Object> blobMetadataOutput; 193 if (processorId != null) { 194 blobMetadataOutput = readMetadata(processorId, blob, blobMetadata, ignorePrefix); 195 196 } else { 197 blobMetadataOutput = readMetadata(blob, blobMetadata, ignorePrefix); 198 } 199 200 // Write doc properties from outputs. 201 for (String metadata : blobMetadataOutput.keySet()) { 202 Object metadataValue = blobMetadataOutput.get(metadata); 203 boolean metadataIsArray = metadataValue instanceof Object[] || metadataValue instanceof List; 204 String property = metadataMapping.get(metadata); 205 if (!(metadataValue instanceof Date) && !(metadataValue instanceof Collection) && !metadataIsArray) { 206 metadataValue = metadataValue.toString(); 207 } 208 if (metadataValue instanceof String) { 209 // sanitize string for PostgreSQL textual storage 210 metadataValue = ((String) metadataValue).replace("\u0000", ""); 211 } 212 try { 213 if (doc.getProperty(property).isList()) { 214 if (!metadataIsArray) { 215 metadataValue = Arrays.asList(metadataValue); 216 } 217 } else { 218 if (metadataIsArray) { 219 if (metadataValue instanceof Object[]) { 220 metadataValue = Arrays.asList((Object[]) metadataValue); 221 } else { 222 metadataValue = metadataValue.toString(); 223 } 224 } 225 } 226 doc.setPropertyValue(property, (Serializable) metadataValue); 227 } catch (PropertyException e) { 228 log.warn(String.format( 229 "Failed to set property '%s' to value %s from metadata '%s' in '%s' in document '%s' ('%s')", 230 property, metadataValue, metadata, mappingDescriptor.getBlobXPath(), doc.getId(), 231 doc.getPath())); 232 } 233 } 234 } 235 } 236 237 /*--------------------- Event Service --------------------------*/ 238 239 @Override 240 public void handleSyncUpdate(DocumentModel doc) { 241 List<MetadataMappingDescriptor> syncMappingDescriptors = getSyncMapping(doc); 242 if (syncMappingDescriptors != null) { 243 handleUpdate(syncMappingDescriptors, doc); 244 } 245 } 246 247 @Override 248 public void handleUpdate(List<MetadataMappingDescriptor> mappingDescriptors, DocumentModel doc) { 249 for (MetadataMappingDescriptor mappingDescriptor : mappingDescriptors) { 250 Property fileProp = doc.getProperty(mappingDescriptor.getBlobXPath()); 251 Blob blob = fileProp.getValue(Blob.class); 252 if (blob != null) { 253 boolean isDirtyMapping = isDirtyMapping(mappingDescriptor, doc); 254 if (isDirtyMapping) { 255 BlobManager blobManager = Framework.getService(BlobManager.class); 256 BlobProvider blobProvider = blobManager.getBlobProvider(blob); 257 // do not write metadata in blobs backed by extended blob providers (ex: Google Drive) or blobs from 258 // providers that prevent user updates 259 if (blobProvider != null && (!blobProvider.supportsUserUpdate() || blobProvider.getBinaryManager() == null)) { 260 return; 261 } 262 // if document metadata dirty, write metadata from doc to Blob 263 Blob newBlob = writeMetadata(mappingDescriptor.getProcessor(), fileProp.getValue(Blob.class), mappingDescriptor.getId(), doc); 264 fileProp.setValue(newBlob); 265 } else if (fileProp.isDirty()) { 266 // if Blob dirty and document metadata not dirty, write metadata from Blob to doc 267 writeMetadata(doc); 268 } 269 } 270 } 271 } 272 273 /*--------------------- Utils --------------------------*/ 274 275 /** 276 * Check for each Binary Rule if the document is accepted or not. 277 * 278 * @return the list of metadata which should be processed sorted by rules order. (high to low priority) 279 */ 280 protected Set<MetadataRuleDescriptor> checkFilter(final ActionContext actionContext) { 281 final ActionManager actionService = Framework.getService(ActionManager.class); 282 return binaryMetadataComponent.ruleRegistry.contribs.stream().filter(ruleDescriptor -> { 283 if (!ruleDescriptor.getEnabled()) { 284 return false; 285 } 286 for (String filterId : ruleDescriptor.getFilterIds()) { 287 if (!actionService.checkFilter(filterId, actionContext)) { 288 return false; 289 } 290 } 291 return true; 292 }).collect(Collectors.toSet()); 293 } 294 295 protected ActionContext createActionContext(DocumentModel doc) { 296 ActionContext actionContext = new ELActionContext(); 297 actionContext.setCurrentDocument(doc); 298 return actionContext; 299 } 300 301 protected BinaryMetadataProcessor getProcessor(String processorId) throws NoSuchMethodException { 302 return binaryMetadataComponent.processorRegistry.getProcessor(processorId); 303 } 304 305 /** 306 * @return Dirty metadata from metadata mapping contribution and handle async processes. 307 */ 308 public List<MetadataMappingDescriptor> getSyncMapping(DocumentModel doc) { 309 // Check if rules applying for this document. 310 ActionContext actionContext = createActionContext(doc); 311 Set<MetadataRuleDescriptor> ruleDescriptors = checkFilter(actionContext); 312 Set<String> syncMappingDescriptorIds = new HashSet<>(); 313 HashSet<String> asyncMappingDescriptorIds = new HashSet<>(); 314 for (MetadataRuleDescriptor ruleDescriptor : ruleDescriptors) { 315 if (ruleDescriptor.getIsAsync()) { 316 asyncMappingDescriptorIds.addAll(ruleDescriptor.getMetadataMappingIdDescriptors()); 317 continue; 318 } 319 syncMappingDescriptorIds.addAll(ruleDescriptor.getMetadataMappingIdDescriptors()); 320 } 321 322 // Handle async rules which should be taken into account in async listener. 323 if (!asyncMappingDescriptorIds.isEmpty()) { 324 doc.putContextData(BinaryMetadataConstants.ASYNC_BINARY_METADATA_EXECUTE, Boolean.TRUE); 325 doc.putContextData(BinaryMetadataConstants.ASYNC_MAPPING_RESULT, 326 (Serializable) getMapping(asyncMappingDescriptorIds)); 327 } 328 329 if (syncMappingDescriptorIds.isEmpty()) { 330 return null; 331 } 332 return getMapping(syncMappingDescriptorIds); 333 } 334 335 protected List<MetadataMappingDescriptor> getMapping(Set<String> mappingDescriptorIds) { 336 // For each mapping descriptors, store mapping. 337 List<MetadataMappingDescriptor> mappingResult = new ArrayList<>(); 338 for (String mappingDescriptorId : mappingDescriptorIds) { 339 if (!binaryMetadataComponent.mappingRegistry.getMappingDescriptorMap().containsKey(mappingDescriptorId)) { 340 log.warn("Missing binary metadata descriptor with id '" + mappingDescriptorId 341 + "'. Or check your rule contribution with proper metadataMapping-id."); 342 continue; 343 } 344 mappingResult.add(binaryMetadataComponent.mappingRegistry.getMappingDescriptorMap().get( 345 mappingDescriptorId)); 346 } 347 return mappingResult; 348 } 349 350 /** 351 * Maps inspector only. 352 */ 353 protected boolean isDirtyMapping(MetadataMappingDescriptor mappingDescriptor, DocumentModel doc) { 354 Map<String, String> mappingResult = new HashMap<>(); 355 for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) { 356 mappingResult.put(metadataDescriptor.getXpath(), metadataDescriptor.getName()); 357 } 358 // Returning only dirty properties 359 HashMap<String, Object> resultDirtyMapping = new HashMap<>(); 360 for (String metadata : mappingResult.keySet()) { 361 Property property = doc.getProperty(metadata); 362 if (property.isDirty()) { 363 resultDirtyMapping.put(mappingResult.get(metadata), doc.getPropertyValue(metadata)); 364 } 365 } 366 return !resultDirtyMapping.isEmpty(); 367 } 368}