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.PropertyException;
043import org.nuxeo.ecm.core.api.model.Property;
044import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
045import org.nuxeo.ecm.platform.actions.ActionContext;
046import org.nuxeo.ecm.platform.actions.ELActionContext;
047import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
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, Object> 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, Object> 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, Object> metadataMapping = new HashMap<>();
127            MetadataMappingDescriptor mappingDescriptor = BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().get(
128                    mappingDescriptorId);
129            for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) {
130                metadataMapping.put(metadataDescriptor.getName(), doc.getPropertyValue(metadataDescriptor.getXpath()));
131            }
132            BinaryMetadataProcessor processor = getProcessor(processorName);
133            return processor.writeMetadata(blob, metadataMapping, mappingDescriptor.getIgnorePrefix());
134        } catch (NoSuchMethodException e) {
135            throw new BinaryMetadataException(e);
136        }
137
138    }
139
140    @Override
141    public Blob writeMetadata(Blob blob, String mappingDescriptorId, DocumentModel doc) {
142        return writeMetadata(BinaryMetadataConstants.EXIF_TOOL_CONTRIBUTION_ID, blob, mappingDescriptorId, doc);
143    }
144
145    @Override
146    public void writeMetadata(DocumentModel doc, CoreSession session) {
147        // Check if rules applying for this document.
148        ActionContext actionContext = createActionContext(doc);
149        Set<MetadataRuleDescriptor> ruleDescriptors = checkFilter(actionContext);
150        List<String> mappingDescriptorIds = new ArrayList<>();
151        for (MetadataRuleDescriptor ruleDescriptor : ruleDescriptors) {
152            mappingDescriptorIds.addAll(ruleDescriptor.getMetadataMappingIdDescriptors());
153        }
154        if (mappingDescriptorIds.isEmpty()) {
155            return;
156        }
157
158        // For each mapping descriptors, overriding mapping document properties.
159        for (String mappingDescriptorId : mappingDescriptorIds) {
160            if (!BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().containsKey(mappingDescriptorId)) {
161                log.warn("Missing binary metadata descriptor with id '" + mappingDescriptorId
162                        + "'. Or check your rule contribution with proper metadataMapping-id.");
163                continue;
164            }
165            writeMetadata(doc, session, mappingDescriptorId);
166        }
167    }
168
169    @Override
170    public void writeMetadata(DocumentModel doc, CoreSession session, String mappingDescriptorId) {
171        // Creating mapping properties Map.
172        Map<String, String> metadataMapping = new HashMap<>();
173        List<String> blobMetadata = new ArrayList<>();
174        MetadataMappingDescriptor mappingDescriptor = BinaryMetadataComponent.self.mappingRegistry.getMappingDescriptorMap().get(
175                mappingDescriptorId);
176        boolean ignorePrefix = mappingDescriptor.getIgnorePrefix();
177        // Extract blob from the contributed xpath
178        Blob blob = doc.getProperty(mappingDescriptor.getBlobXPath()).getValue(Blob.class);
179        if (blob != null && mappingDescriptor.getMetadataDescriptors() != null
180                && !mappingDescriptor.getMetadataDescriptors().isEmpty()) {
181            for (MetadataMappingDescriptor.MetadataDescriptor metadataDescriptor : mappingDescriptor.getMetadataDescriptors()) {
182                metadataMapping.put(metadataDescriptor.getName(), metadataDescriptor.getXpath());
183                blobMetadata.add(metadataDescriptor.getName());
184            }
185
186            // Extract metadata from binary.
187            String processorId = mappingDescriptor.getProcessor();
188            Map<String, Object> blobMetadataOutput;
189            if (processorId != null) {
190                blobMetadataOutput = readMetadata(processorId, blob, blobMetadata, ignorePrefix);
191
192            } else {
193                blobMetadataOutput = readMetadata(blob, blobMetadata, ignorePrefix);
194            }
195
196            // Write doc properties from outputs.
197            for (String metadata : blobMetadataOutput.keySet()) {
198                Object metadataValue = blobMetadataOutput.get(metadata);
199                String property = metadataMapping.get(metadata);
200                if (!(metadataValue instanceof Date) && !(metadataValue instanceof Collection)
201                        && !(metadataValue.getClass().isArray())) {
202                    metadataValue = metadataValue.toString();
203                }
204                try {
205                    doc.setPropertyValue(property, (Serializable) metadataValue);
206                } catch (PropertyException e) {
207                    log.warn(String.format(
208                            "Failed to set property '%s' to value %s from metadata '%s' in '%s' in document '%s' ('%s')",
209                            property, metadataValue, metadata, mappingDescriptor.getBlobXPath(), doc.getId(),
210                            doc.getPath()));
211                }
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(mappingDescriptor.getProcessor(), 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(mappingDescriptor.getProcessor(), 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();
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}