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