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