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