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