001/*
002 * (C) Copyright 2006-2007 Nuxeo SAS (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.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 *     Nuxeo - initial API and implementation
016 *
017 * $Id: LiveEditBootstrapHelper.java 30586 2008-02-26 14:30:17Z ogrisel $
018 */
019
020package org.nuxeo.ecm.webapp.liveedit;
021
022import static org.jboss.seam.ScopeType.EVENT;
023
024import java.io.IOException;
025import java.io.Serializable;
026import java.util.Calendar;
027import java.util.Enumeration;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031
032import javax.faces.context.FacesContext;
033import javax.servlet.http.Cookie;
034import javax.servlet.http.HttpServletRequest;
035import javax.servlet.http.HttpServletResponse;
036
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.dom4j.Document;
040import org.dom4j.DocumentFactory;
041import org.dom4j.Element;
042import org.dom4j.QName;
043import org.dom4j.io.OutputFormat;
044import org.dom4j.io.XMLWriter;
045import org.jboss.seam.ScopeType;
046import org.jboss.seam.annotations.Factory;
047import org.jboss.seam.annotations.In;
048import org.jboss.seam.annotations.Name;
049import org.jboss.seam.annotations.web.RequestParameter;
050import org.jboss.seam.annotations.Scope;
051import org.nuxeo.ecm.core.api.Blob;
052import org.nuxeo.ecm.core.api.CoreInstance;
053import org.nuxeo.ecm.core.api.CoreSession;
054import org.nuxeo.ecm.core.api.DocumentModel;
055import org.nuxeo.ecm.core.api.DocumentNotFoundException;
056import org.nuxeo.ecm.core.api.IdRef;
057import org.nuxeo.ecm.core.api.LifeCycleConstants;
058import org.nuxeo.ecm.core.api.NuxeoException;
059import org.nuxeo.ecm.core.api.PropertyException;
060import org.nuxeo.ecm.core.api.security.SecurityConstants;
061import org.nuxeo.ecm.core.schema.FacetNames;
062import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeEntry;
063import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
064import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
065import org.nuxeo.ecm.platform.ui.web.tag.fn.LiveEditConstants;
066import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
067import org.nuxeo.runtime.api.Framework;
068
069/**
070 * The LiveEdit bootstrap procedure works as follows:
071 * <ul>
072 * <li>browsed page calls a JSF function from the DocumentModelFunctions class (edit a document, create new document,
073 * etc.) to generate;</li>
074 * <li>composing a specific URL as result, triggering the bootstrap addon to popup;</li>
075 * <li>the addon come back with the URL composed allowing the present seam component to create the bootstrap file. The
076 * file contains various data as requested in the URL;</li>
077 * <li>the XML file is now available to addon which presents it to the client plugin.</li>
078 * </ul>
079 * Please refer to the nuxeo book chapter on desktop integration for details on the format of the nxedit URLs and the
080 * XML bootstrap file.
081 *
082 * @author Thierry Delprat NXP-1959 the bootstrap file is managing the 'create new document [from template]' case too.
083 *         The URL is containing an action identifier.
084 * @author Rux rdarlea@nuxeo.com
085 * @author Olivier Grisel ogrisel@nuxeo.com (split url functions into JSF DocumentModelFunctions module)
086 */
087@Scope(EVENT)
088@Name("liveEditHelper")
089public class LiveEditBootstrapHelper implements Serializable, LiveEditConstants {
090
091    protected static final String MODIFIED_FIELD = "modified";
092
093    protected static final String DUBLINCORE_SCHEMA = "dublincore";
094
095    private static final Log log = LogFactory.getLog(LiveEditBootstrapHelper.class);
096
097    private static final long serialVersionUID = 876879071L;
098
099    @In(create = true)
100    protected transient NavigationContext navigationContext;
101
102    @In(create = true, required = false)
103    protected transient CoreSession documentManager;
104
105    @RequestParameter
106    protected String action;
107
108    @RequestParameter
109    protected String repoID;
110
111    @RequestParameter
112    protected String templateRepoID;
113
114    @RequestParameter
115    protected String docRef;
116
117    @RequestParameter
118    protected String templateDocRef;
119
120    @In(create = true)
121    protected LiveEditClientConfig liveEditClientConfig;
122
123    /**
124     * @deprecated use blobPropertyField and filenamePropertyField instead
125     */
126    @Deprecated
127    @RequestParameter
128    protected String schema;
129
130    @RequestParameter
131    protected String templateSchema;
132
133    /**
134     * @deprecated use blobPropertyField instead
135     */
136    @Deprecated
137    @RequestParameter
138    protected String blobField;
139
140    @RequestParameter
141    protected String blobPropertyName;
142
143    @RequestParameter
144    protected String templateBlobField;
145
146    // TODO: to be deprecated once all filenames are stored in the blob itself
147    /**
148     * @deprecated use filenamePropertyField instead
149     */
150    @Deprecated
151    @RequestParameter
152    protected String filenameField;
153
154    // TODO: to be deprecated once all filenames are stored in the blob itself
155    @RequestParameter
156    protected String filenamePropertyName;
157
158    @RequestParameter
159    protected String mimetype;
160
161    @RequestParameter
162    protected String docType;
163
164    protected MimetypeRegistry mimetypeRegistry;
165
166    // Event-long cache for mimetype lookups - no invalidation required
167    protected final Map<String, Boolean> cachedEditableStates = new HashMap<String, Boolean>();
168
169    // Event-long cache for document field lookups - no invalidation required
170    protected final Map<String, Boolean> cachedEditableBlobs = new HashMap<String, Boolean>();
171
172    /**
173     * Creates the bootstrap file. It is called from the browser's addon. The URL composition tells the case and what to
174     * create. The structure is depicted in the NXP-1881. Rux NXP-1959: add new tag on root level describing the action:
175     * actionEdit, actionNew or actionFromTemplate.
176     *
177     * @return the bootstrap file content
178     */
179    public void getBootstrap() throws IOException {
180        String currentRepoID = documentManager.getRepositoryName();
181
182        CoreSession session = documentManager;
183        CoreSession templateSession = documentManager;
184        try {
185            if (repoID != null && !currentRepoID.equals(repoID)) {
186                session = CoreInstance.openCoreSession(repoID);
187            }
188
189            if (templateRepoID != null && !currentRepoID.equals(templateRepoID)) {
190                templateSession = CoreInstance.openCoreSession(templateRepoID);
191            }
192
193            DocumentModel doc = null;
194            DocumentModel templateDoc = null;
195            String filename = null;
196            if (ACTION_EDIT_DOCUMENT.equals(action)) {
197                // fetch the document to edit to get its mimetype and document
198                // type
199                doc = session.getDocument(new IdRef(docRef));
200                docType = doc.getType();
201                Blob blob = null;
202                if (blobPropertyName != null) {
203                    blob = (Blob) doc.getPropertyValue(blobPropertyName);
204                    if (blob == null) {
205                        throw new NuxeoException(String.format("could not find blob to edit with property '%s'",
206                                blobPropertyName));
207                    }
208                } else {
209                    blob = (Blob) doc.getProperty(schema, blobField);
210                    if (blob == null) {
211                        throw new NuxeoException(String.format(
212                                "could not find blob to edit with schema '%s' and field '%s'", schema, blobField));
213                    }
214                }
215                mimetype = blob.getMimeType();
216                if (filenamePropertyName != null) {
217                    filename = (String) doc.getPropertyValue(filenamePropertyName);
218                } else {
219                    filename = (String) doc.getProperty(schema, filenameField);
220                }
221            } else if (ACTION_CREATE_DOCUMENT.equals(action)) {
222                // creating a new document all parameters are read from the
223                // request parameters
224            } else if (ACTION_CREATE_DOCUMENT_FROM_TEMPLATE.equals(action)) {
225                // fetch the template blob to get its mimetype
226                templateDoc = templateSession.getDocument(new IdRef(templateDocRef));
227                Blob blob = (Blob) templateDoc.getProperty(templateSchema, templateBlobField);
228                if (blob == null) {
229                    throw new NuxeoException(String.format(
230                            "could not find template blob with schema '%s' and field '%s'", templateSchema,
231                            templateBlobField));
232                }
233                mimetype = blob.getMimeType();
234                // leave docType from the request query parameter
235            } else {
236                throw new NuxeoException(String.format(
237                        "action '%s' is not a valid LiveEdit action: should be one of '%s', '%s' or '%s'", action,
238                        ACTION_CREATE_DOCUMENT, ACTION_CREATE_DOCUMENT_FROM_TEMPLATE, ACTION_EDIT_DOCUMENT));
239            }
240
241            FacesContext context = FacesContext.getCurrentInstance();
242            HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
243            HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
244
245            Element root = DocumentFactory.getInstance().createElement(liveEditTag);
246            root.addNamespace("", XML_LE_NAMESPACE);
247            // RUX NXP-1959: action id
248            Element actionInfo = root.addElement(actionSelectorTag);
249            actionInfo.setText(action);
250
251            // Document related informations
252            Element docInfo = root.addElement(documentTag);
253            addTextElement(docInfo, docRefTag, docRef);
254            Element docPathT = docInfo.addElement(docPathTag);
255            Element docTitleT = docInfo.addElement(docTitleTag);
256            if (doc != null) {
257                docPathT.setText(doc.getPathAsString());
258                docTitleT.setText(doc.getTitle());
259            }
260            addTextElement(docInfo, docRepositoryTag, repoID);
261
262            addTextElement(docInfo, docSchemaNameTag, schema);
263            addTextElement(docInfo, docFieldNameTag, blobField);
264            addTextElement(docInfo, docBlobFieldNameTag, blobField);
265            Element docFieldPathT = docInfo.addElement(docfieldPathTag);
266            Element docBlobFieldPathT = docInfo.addElement(docBlobFieldPathTag);
267            if (blobPropertyName != null) {
268                // FIXME AT: NXP-2306: send blobPropertyName correctly (?)
269                docFieldPathT.setText(blobPropertyName);
270                docBlobFieldPathT.setText(blobPropertyName);
271            } else {
272                if (schema != null && blobField != null) {
273                    docFieldPathT.setText(schema + ':' + blobField);
274                    docBlobFieldPathT.setText(schema + ':' + blobField);
275                }
276            }
277            addTextElement(docInfo, docFilenameFieldNameTag, filenameField);
278            Element docFilenameFieldPathT = docInfo.addElement(docFilenameFieldPathTag);
279            if (filenamePropertyName != null) {
280                docFilenameFieldPathT.setText(filenamePropertyName);
281            } else {
282                if (schema != null && blobField != null) {
283                    docFilenameFieldPathT.setText(schema + ':' + filenameField);
284                }
285            }
286
287            addTextElement(docInfo, docfileNameTag, filename);
288            addTextElement(docInfo, docTypeTag, docType);
289            addTextElement(docInfo, docMimetypeTag, mimetype);
290            addTextElement(docInfo, docFileExtensionTag, getFileExtension(mimetype));
291
292            Element docFileAuthorizedExtensions = docInfo.addElement(docFileAuthorizedExtensionsTag);
293            List<String> authorizedExtensions = getFileExtensions(mimetype);
294            if (authorizedExtensions != null) {
295                for (String extension : authorizedExtensions) {
296                    addTextElement(docFileAuthorizedExtensions, docFileAuthorizedExtensionTag, extension);
297                }
298            }
299
300            Element docIsVersionT = docInfo.addElement(docIsVersionTag);
301            Element docIsLockedT = docInfo.addElement(docIsLockedTag);
302            if (ACTION_EDIT_DOCUMENT.equals(action)) {
303                docIsVersionT.setText(Boolean.toString(doc.isVersion()));
304                docIsLockedT.setText(Boolean.toString(doc.isLocked()));
305            }
306
307            // template information for ACTION_CREATE_DOCUMENT_FROM_TEMPLATE
308
309            Element templateDocInfo = root.addElement(templateDocumentTag);
310            addTextElement(templateDocInfo, docRefTag, templateDocRef);
311            docPathT = templateDocInfo.addElement(docPathTag);
312            docTitleT = templateDocInfo.addElement(docTitleTag);
313            if (templateDoc != null) {
314                docPathT.setText(templateDoc.getPathAsString());
315                docTitleT.setText(templateDoc.getTitle());
316            }
317            addTextElement(templateDocInfo, docRepositoryTag, templateRepoID);
318            addTextElement(templateDocInfo, docSchemaNameTag, templateSchema);
319            addTextElement(templateDocInfo, docFieldNameTag, templateBlobField);
320            addTextElement(templateDocInfo, docBlobFieldNameTag, templateBlobField);
321            docFieldPathT = templateDocInfo.addElement(docfieldPathTag);
322            docBlobFieldPathT = templateDocInfo.addElement(docBlobFieldPathTag);
323            if (templateSchema != null && templateBlobField != null) {
324                docFieldPathT.setText(templateSchema + ":" + templateBlobField);
325                docBlobFieldPathT.setText(templateSchema + ":" + templateBlobField);
326            }
327            addTextElement(templateDocInfo, docMimetypeTag, mimetype);
328            addTextElement(templateDocInfo, docFileExtensionTag, getFileExtension(mimetype));
329
330            Element templateFileAuthorizedExtensions = templateDocInfo.addElement(docFileAuthorizedExtensionsTag);
331            if (authorizedExtensions != null) {
332                for (String extension : authorizedExtensions) {
333                    addTextElement(templateFileAuthorizedExtensions, docFileAuthorizedExtensionTag, extension);
334                }
335            }
336
337            // Browser request related informations
338            Element requestInfo = root.addElement(requestInfoTag);
339            Cookie[] cookies = request.getCookies();
340            Element cookiesT = requestInfo.addElement(requestCookiesTag);
341            for (Cookie cookie : cookies) {
342                Element cookieT = cookiesT.addElement(requestCookieTag);
343                cookieT.addAttribute("name", cookie.getName());
344                cookieT.setText(cookie.getValue());
345            }
346            Element headersT = requestInfo.addElement(requestHeadersTag);
347            Enumeration hEnum = request.getHeaderNames();
348            while (hEnum.hasMoreElements()) {
349                String hName = (String) hEnum.nextElement();
350                if (!hName.equalsIgnoreCase("cookie")) {
351                    Element headerT = headersT.addElement(requestHeaderTag);
352                    headerT.addAttribute("name", hName);
353                    headerT.setText(request.getHeader(hName));
354                }
355            }
356            addTextElement(requestInfo, requestBaseURLTag, BaseURL.getBaseURL(request));
357
358            // User related informations
359            String username = context.getExternalContext().getUserPrincipal().getName();
360            Element userInfo = root.addElement(userInfoTag);
361            addTextElement(userInfo, userNameTag, username);
362            addTextElement(userInfo, userPasswordTag, "");
363            addTextElement(userInfo, userTokenTag, "");
364            addTextElement(userInfo, userLocaleTag, context.getViewRoot().getLocale().toString());
365            // Rux NXP-1882: the wsdl locations
366            String baseUrl = BaseURL.getBaseURL(request);
367            Element wsdlLocations = root.addElement(wsdlLocationsTag);
368            Element wsdlAccessWST = wsdlLocations.addElement(wsdlAccessWebServiceTag);
369            wsdlAccessWST.setText(baseUrl + "webservices/nuxeoAccess?wsdl");
370            Element wsdlEEWST = wsdlLocations.addElement(wsdlLEWebServiceTag);
371            wsdlEEWST.setText(baseUrl + "webservices/nuxeoLEWS?wsdl");
372
373            // Server related informations
374            Element serverInfo = root.addElement(serverInfoTag);
375            Element serverVersionT = serverInfo.addElement(serverVersionTag);
376            serverVersionT.setText("5.1"); // TODO: use a buildtime generated
377            // version tag instead
378
379            // Client related informations
380            Element editId = root.addElement(editIdTag);
381            editId.setText(getEditId(doc, session, username));
382
383            // serialize bootstrap XML document in the response
384            Document xmlDoc = DocumentFactory.getInstance().createDocument();
385            xmlDoc.setRootElement(root);
386            response.setContentType("text/xml; charset=UTF-8");
387
388            // use a formatter to make it easier to debug live edit client
389            // implementations
390            OutputFormat format = OutputFormat.createPrettyPrint();
391            format.setEncoding("UTF-8");
392            XMLWriter writer = new XMLWriter(response.getOutputStream(), format);
393            writer.write(xmlDoc);
394
395            response.flushBuffer();
396            context.responseComplete();
397        } finally {
398            if (session != null && session != documentManager) {
399                session.close();
400            }
401            if (templateSession != null && templateSession != documentManager) {
402                templateSession.close();
403            }
404        }
405    }
406
407    protected String getFileExtension(String mimetype) {
408        if (mimetype == null) {
409            return null;
410        }
411        MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
412        List<String> extensions = mimetypeRegistry.getExtensionsFromMimetypeName(mimetype);
413        if (extensions != null && !extensions.isEmpty()) {
414            return extensions.get(0);
415        } else {
416            return null;
417        }
418    }
419
420    protected List<String> getFileExtensions(String mimetype) {
421        if (mimetype == null) {
422            return null;
423        }
424        MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
425        List<String> extensions = mimetypeRegistry.getExtensionsFromMimetypeName(mimetype);
426        return extensions;
427    }
428
429    protected static Element addTextElement(Element parent, QName newElementName, String value) {
430        Element element = parent.addElement(newElementName);
431        if (value != null) {
432            element.setText(value);
433        }
434        return element;
435    }
436
437    // TODO: please explain what is the use of the "editId" tag here
438    protected static String getEditId(DocumentModel doc, CoreSession session, String userName) {
439        StringBuilder sb = new StringBuilder();
440
441        if (doc != null) {
442            sb.append(doc.getId());
443        } else {
444            sb.append("NewDocument");
445        }
446        sb.append('-');
447        sb.append(session.getRepositoryName());
448        sb.append('-');
449        sb.append(userName);
450        Calendar modified = null;
451        if (doc != null) {
452            try {
453                modified = (Calendar) doc.getProperty(DUBLINCORE_SCHEMA, MODIFIED_FIELD);
454            } catch (PropertyException e) {
455                modified = null;
456            }
457        }
458        if (modified == null) {
459            modified = Calendar.getInstance();
460        }
461        sb.append('-');
462        sb.append(modified.getTimeInMillis());
463        return sb.toString();
464    }
465
466    //
467    // Methods to check whether or not to display live edit links
468    //
469
470    /**
471     * @deprecated use {@link #isLiveEditable(DocumentModel doc, String blobXpath)}
472     */
473    @Deprecated
474    public boolean isLiveEditable(Blob blob) {
475        if (blob == null) {
476            return false;
477        }
478        String mimetype = blob.getMimeType();
479        return isMimeTypeLiveEditable(mimetype);
480    }
481
482    /**
483     * @param document the document to edit.
484     * @param blobXPath XPath to the blob property
485     * @return true if the document is immutable and the blob's mime type is supported, false otherwise.
486     * @since 5.4
487     */
488    public boolean isLiveEditable(DocumentModel document, Blob blob) {
489        if (document.isImmutable()) {
490            return false;
491        }
492        // NXP-14476: Testing lifecycle state is part of the "mutable_document" filter
493        if (document.getCurrentLifeCycleState().equals(LifeCycleConstants.DELETED_STATE)) {
494            return false;
495        }
496        if (blob == null) {
497            return false;
498        }
499        String mimetype = blob.getMimeType();
500        return isMimeTypeLiveEditable(mimetype);
501    }
502
503    public boolean isMimeTypeLiveEditable(Blob blob) {
504        if (blob == null) {
505            return false;
506        }
507        String mimetype = blob.getMimeType();
508        return isMimeTypeLiveEditable(mimetype);
509    }
510
511    public boolean isMimeTypeLiveEditable(String mimetype) {
512
513        Boolean isEditable = cachedEditableStates.get(mimetype);
514        if (isEditable == null) {
515
516            if (liveEditClientConfig.getLiveEditConfigurationPolicy().equals(LiveEditClientConfig.LE_CONFIG_CLIENTSIDE)) {
517                // only trust client config
518                isEditable = liveEditClientConfig.isMimeTypeLiveEditable(mimetype);
519                cachedEditableStates.put(mimetype, isEditable);
520                return isEditable;
521            }
522
523            MimetypeEntry mimetypeEntry = getMimetypeRegistry().getMimetypeEntryByMimeType(mimetype);
524            if (mimetypeEntry == null) {
525                isEditable = Boolean.FALSE;
526            } else {
527                isEditable = mimetypeEntry.isOnlineEditable();
528            }
529
530            if (liveEditClientConfig.getLiveEditConfigurationPolicy().equals(LiveEditClientConfig.LE_CONFIG_BOTHSIDES)) {
531                boolean isEditableOnClient = liveEditClientConfig.isMimeTypeLiveEditable(mimetype);
532                isEditable = isEditable && isEditableOnClient;
533            }
534            cachedEditableStates.put(mimetype, isEditable);
535        }
536        return isEditable;
537    }
538
539    @Factory(value = "msword_liveeditable", scope = ScopeType.SESSION)
540    public boolean isMSWordLiveEdititable() {
541        return isMimeTypeLiveEditable("application/msword");
542    }
543
544    @Factory(value = "msexcel_liveeditable", scope = ScopeType.SESSION)
545    public boolean isMSExcelLiveEdititable() {
546        return isMimeTypeLiveEditable("application/vnd.ms-excel");
547    }
548
549    @Factory(value = "mspowerpoint_liveeditable", scope = ScopeType.SESSION)
550    public boolean isMSPowerpointLiveEdititable() {
551        return isMimeTypeLiveEditable("application/vnd.ms-powerpoint");
552    }
553
554    @Factory(value = "ootext_liveeditable", scope = ScopeType.SESSION)
555    public boolean isOOTextLiveEdititable() {
556        return isMimeTypeLiveEditable("application/vnd.oasis.opendocument.text");
557    }
558
559    @Factory(value = "oocalc_liveeditable", scope = ScopeType.SESSION)
560    public boolean isOOCalcLiveEdititable() {
561        return isMimeTypeLiveEditable("application/vnd.oasis.opendocument.spreadsheet");
562    }
563
564    @Factory(value = "oopresentation_liveeditable", scope = ScopeType.SESSION)
565    public boolean isOOPresentationLiveEdititable() {
566        return isMimeTypeLiveEditable("application/vnd.oasis.opendocument.presentation");
567    }
568
569    public boolean isCurrentDocumentLiveEditable() {
570        return isDocumentLiveEditable(navigationContext.getCurrentDocument(), DEFAULT_SCHEMA, DEFAULT_BLOB_FIELD);
571    }
572
573    public boolean isCurrentDocumentLiveEditable(String schemaName, String fieldName) {
574        return isDocumentLiveEditable(navigationContext.getCurrentDocument(), schemaName, fieldName);
575    }
576
577    public boolean isCurrentDocumentLiveEditable(String propertyName) {
578        return isDocumentLiveEditable(navigationContext.getCurrentDocument(), propertyName);
579    }
580
581    public boolean isDocumentLiveEditable(DocumentModel documentModel, String schemaName, String fieldName)
582            {
583        return isDocumentLiveEditable(documentModel, schemaName + ":" + fieldName);
584    }
585
586    public boolean isDocumentLiveEditable(DocumentModel documentModel, String propertyName) {
587        if (documentModel == null) {
588            log.warn("cannot check live editable state of null DocumentModel");
589            return false;
590        }
591
592        // NXP-14476: Testing lifecycle state is part of the "mutable_document" filter
593        if (documentModel.getCurrentLifeCycleState().equals(LifeCycleConstants.DELETED_STATE)) {
594            return false;
595        }
596
597        // check Client browser config
598        if (!liveEditClientConfig.isLiveEditInstalled()) {
599            return false;
600        }
601
602        String cacheKey = documentModel.getRef() + "__" + propertyName;
603        Boolean cachedEditableBlob = cachedEditableBlobs.get(cacheKey);
604        if (cachedEditableBlob == null) {
605
606            if (documentModel.hasFacet(FacetNames.IMMUTABLE)) {
607                return cacheBlobToFalse(cacheKey);
608            }
609
610            if (!documentManager.hasPermission(documentModel.getRef(), SecurityConstants.WRITE_PROPERTIES)) {
611                // the lock state is check as a extension to the
612                // SecurityPolicyManager
613                return cacheBlobToFalse(cacheKey);
614            }
615
616            Blob blob;
617            try {
618                blob = documentModel.getProperty(propertyName).getValue(Blob.class);
619            } catch (PropertyException e) {
620                // this document cannot host a live editable blob is the
621                // requested property, ignore
622                return cacheBlobToFalse(cacheKey);
623            }
624            cachedEditableBlob = isLiveEditable(blob);
625            cachedEditableBlobs.put(cacheKey, cachedEditableBlob);
626        }
627        return cachedEditableBlob;
628    }
629
630    protected boolean cacheBlobToFalse(String cacheKey) {
631        cachedEditableBlobs.put(cacheKey, Boolean.FALSE);
632        return false;
633    }
634
635    protected MimetypeRegistry getMimetypeRegistry() {
636        if (mimetypeRegistry == null) {
637            mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
638        }
639        return mimetypeRegistry;
640    }
641
642}