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}