001/*
002 * (C) Copyright 2008 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 *     Nuxeo - initial API and implementation
018 *
019 * $Id$
020 */
021package org.nuxeo.ecm.platform.filemanager.core.listener;
022
023import java.io.Serializable;
024import java.util.Iterator;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.nuxeo.ecm.core.api.Blob;
029import org.nuxeo.ecm.core.api.DocumentModel;
030import org.nuxeo.ecm.core.api.PropertyException;
031import org.nuxeo.ecm.core.api.model.Property;
032import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
033import org.nuxeo.ecm.core.event.Event;
034import org.nuxeo.ecm.core.event.EventContext;
035import org.nuxeo.ecm.core.event.EventListener;
036import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
037import org.nuxeo.ecm.core.schema.FacetNames;
038import org.nuxeo.ecm.core.utils.BlobsExtractor;
039import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeEntry;
040import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
041import org.nuxeo.ecm.platform.types.Type;
042import org.nuxeo.ecm.platform.types.TypeManager;
043import org.nuxeo.runtime.api.Framework;
044
045/**
046 * Listener responsible for computing the mimetype of a new or edited blob and the common:icon field if necessary.
047 * <p>
048 * The common:size is also maintained as the length of the main blob to preserve backward compatibility.
049 * <p>
050 * The logic of this event listener is divided into static public methods to make it easy to override this event
051 * listener with a custom implementation.
052 *
053 * @author ogrisel
054 */
055public class MimetypeIconUpdater implements EventListener {
056
057    protected Log log = LogFactory.getLog(MimetypeIconUpdater.class);
058
059    public static final String ICON_SCHEMA = "common";
060
061    public static final String ICON_FIELD = ICON_SCHEMA + ":" + "icon";
062
063    public static final String MAIN_BLOB_FIELD = "file:content";
064
065    public static final String MAIN_BLOB_SCHEMA = "file";
066
067    @Deprecated
068    // the length of the main blob is now stored inside the blob itself
069    private static final String SIZE_FIELD = "common:size";
070
071    @Deprecated
072    // the filename should now be stored inside the main blob
073    public static final String MAIN_EXTERNAL_FILENAME_FIELD = "file:filename";
074
075    protected static final String OCTET_STREAM_MT = "application/octet-stream";
076
077    public final BlobsExtractor blobExtractor = new BlobsExtractor();
078
079    MimetypeRegistry mimetypeService;
080
081    public MimetypeRegistry getMimetypeRegistry() {
082        if (mimetypeService == null) {
083            mimetypeService = Framework.getService(MimetypeRegistry.class);
084        }
085
086        return mimetypeService;
087    }
088
089    public void handleEvent(Event event) {
090
091        EventContext ctx = event.getContext();
092        if (ctx instanceof DocumentEventContext) {
093
094            DocumentEventContext docCtx = (DocumentEventContext) ctx;
095            DocumentModel doc = docCtx.getSourceDocument();
096
097            // Don't update icon for immutable documents
098            if (doc.hasFacet(FacetNames.IMMUTABLE)) {
099                return;
100            }
101
102            // BBB: handle old filename scheme
103            updateFilename(doc);
104
105            try {
106                // ensure the document main icon is not null
107                setDefaultIcon(doc);
108
109                // update mimetypes of blobs in the document
110                for (Property prop : blobExtractor.getBlobsProperties(doc)) {
111                    if (prop.isDirty()) {
112                        updateBlobProperty(doc, getMimetypeRegistry(), prop);
113                    }
114                }
115
116                // update the document icon and size according to the main blob
117                if (doc.hasSchema(MAIN_BLOB_SCHEMA) && doc.getProperty(MAIN_BLOB_FIELD).isDirty()) {
118                    updateIconAndSizeFields(doc, getMimetypeRegistry(),
119                            doc.getProperty(MAIN_BLOB_FIELD).getValue(Blob.class));
120                }
121            } catch (PropertyException e) {
122                e.addInfo("Error in MimetypeIconUpdater listener");
123                throw e;
124            }
125        }
126    }
127
128    /**
129     * Recursively call updateBlobProperty on every dirty blob embedded as direct children or contained in one of the
130     * container children.
131     *
132     * @deprecated now we use {@link BlobsExtractor} that cache path fields.
133     */
134    @Deprecated
135    // TODO: remove
136    public void recursivelyUpdateBlobs(DocumentModel doc, MimetypeRegistry mimetypeService,
137            Iterator<Property> dirtyChildren) {
138        while (dirtyChildren.hasNext()) {
139            Property dirtyProperty = dirtyChildren.next();
140            if (dirtyProperty instanceof BlobProperty) {
141                updateBlobProperty(doc, mimetypeService, dirtyProperty);
142            } else if (dirtyProperty.isContainer()) {
143                recursivelyUpdateBlobs(doc, mimetypeService, dirtyProperty.getDirtyChildren());
144            }
145        }
146    }
147
148    /**
149     * Update the mimetype of a blob along with the icon and size fields of the document if the blob is the main blob of
150     * the document.
151     */
152    public void updateBlobProperty(DocumentModel doc, MimetypeRegistry mimetypeService, Property dirtyProperty) {
153        String fieldPath = dirtyProperty.getPath();
154        // cas shema without prefix : we need to add schema name as prefix
155        if (!fieldPath.contains(":")) {
156            fieldPath = dirtyProperty.getSchema().getName() + ":" + fieldPath.substring(1);
157        }
158
159        Blob blob = dirtyProperty.getValue(Blob.class);
160        if (blob != null && (blob.getMimeType() == null || blob.getMimeType().equals(OCTET_STREAM_MT))) {
161            // update the mimetype (if not set) using the the mimetype registry
162            // service
163            blob = mimetypeService.updateMimetype(blob);
164            doc.setPropertyValue(fieldPath, (Serializable) blob);
165        }
166    }
167
168    private void updateIconAndSizeFields(DocumentModel doc, MimetypeRegistry mimetypeService, Blob blob)
169            throws PropertyException {
170        // update the icon field of the document
171        if (blob != null && !doc.isFolder()) {
172            MimetypeEntry mimetypeEntry = mimetypeService.getMimetypeEntryByMimeType(blob.getMimeType());
173            updateIconField(mimetypeEntry, doc);
174        } else {
175            // reset to document type icon
176            updateIconField(null, doc);
177        }
178
179        // BBB: update the deprecated common:size field to preserver
180        // backward compatibility (we should only use
181        // file:content/length instead)
182        doc.setPropertyValue(SIZE_FIELD, blob != null ? blob.getLength() : 0);
183    }
184
185    /**
186     * Backward compatibility for external filename field: if edited, it might affect the main blob mimetype
187     */
188    public void updateFilename(DocumentModel doc) throws PropertyException {
189
190        if (doc.hasSchema(MAIN_BLOB_FIELD.split(":")[0])) {
191            Property filenameProperty = doc.getProperty(MAIN_EXTERNAL_FILENAME_FIELD);
192            if (filenameProperty.isDirty()) {
193                String filename = filenameProperty.getValue(String.class);
194                if (doc.getProperty(MAIN_BLOB_FIELD).getValue() != null) {
195                    Blob blob = doc.getProperty(MAIN_BLOB_FIELD).getValue(Blob.class);
196                    blob.setFilename(filename);
197                    doc.setPropertyValue(MAIN_BLOB_FIELD, (Serializable) blob);
198                }
199            }
200        }
201    }
202
203    /**
204     * If the icon field is empty, initialize it to the document type icon
205     */
206    public void setDefaultIcon(DocumentModel doc) {
207        if (doc.hasSchema(ICON_SCHEMA) && doc.getProperty(ICON_FIELD).getValue(String.class) == null) {
208            updateIconField(null, doc);
209        }
210    }
211
212    /**
213     * Compute the main icon of a Nuxeo document based on the mimetype of the main attached blob with of fallback on the
214     * document type generic icon.
215     */
216    public void updateIconField(MimetypeEntry mimetypeEntry, DocumentModel doc) {
217        String iconPath = null;
218        if (mimetypeEntry != null && mimetypeEntry.getIconPath() != null) {
219            iconPath = "/icons/" + mimetypeEntry.getIconPath();
220        } else {
221            TypeManager typeManager = Framework.getService(TypeManager.class);
222            if (typeManager == null) {
223                return;
224            }
225            Type uiType = typeManager.getType(doc.getType());
226            if (uiType != null) {
227                iconPath = uiType.getIcon();
228            }
229        }
230        if (iconPath != null && doc.hasSchema(ICON_SCHEMA)) {
231            doc.setPropertyValue(ICON_FIELD, iconPath);
232        }
233    }
234
235}