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