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