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