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