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