001/*
002 * (C) Copyright 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: DocumentModelFunctions.java 30568 2008-02-25 18:52:49Z ogrisel $
020 */
021
022package org.nuxeo.ecm.platform.ui.web.tag.fn;
023
024import java.io.Serializable;
025import java.io.UnsupportedEncodingException;
026import java.net.URLEncoder;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Map;
033
034import javax.faces.context.FacesContext;
035import javax.servlet.http.Cookie;
036import javax.servlet.http.HttpServletRequest;
037
038import org.apache.commons.lang.StringUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.jboss.seam.Component;
042import org.jboss.seam.ScopeType;
043import org.jboss.seam.core.Manager;
044import org.nuxeo.ecm.core.NXCore;
045import org.nuxeo.ecm.core.api.Blob;
046import org.nuxeo.ecm.core.api.CoreSession;
047import org.nuxeo.ecm.core.api.DocumentLocation;
048import org.nuxeo.ecm.core.api.DocumentModel;
049import org.nuxeo.ecm.core.api.DocumentNotFoundException;
050import org.nuxeo.ecm.core.api.IdRef;
051import org.nuxeo.ecm.core.api.LifeCycleException;
052import org.nuxeo.ecm.core.api.NuxeoException;
053import org.nuxeo.ecm.core.api.PropertyException;
054import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl;
055import org.nuxeo.ecm.core.api.model.DocumentPart;
056import org.nuxeo.ecm.core.api.model.Property;
057import org.nuxeo.ecm.core.api.security.SecurityConstants;
058import org.nuxeo.ecm.core.io.download.DownloadService;
059import org.nuxeo.ecm.core.lifecycle.LifeCycle;
060import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
061import org.nuxeo.ecm.core.schema.FacetNames;
062import org.nuxeo.ecm.core.schema.SchemaManager;
063import org.nuxeo.ecm.core.schema.types.Field;
064import org.nuxeo.ecm.core.schema.types.ListType;
065import org.nuxeo.ecm.core.schema.types.Schema;
066import org.nuxeo.ecm.core.schema.types.Type;
067import org.nuxeo.ecm.core.utils.DocumentModelUtils;
068import org.nuxeo.ecm.directory.DirectoryException;
069import org.nuxeo.ecm.directory.Session;
070import org.nuxeo.ecm.directory.api.DirectoryService;
071import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeEntry;
072import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
073import org.nuxeo.ecm.platform.types.TypeManager;
074import org.nuxeo.ecm.platform.types.adapter.TypeInfo;
075import org.nuxeo.ecm.platform.ui.web.directory.DirectoryFunctions;
076import org.nuxeo.ecm.platform.ui.web.directory.DirectoryHelper;
077import org.nuxeo.ecm.platform.ui.web.rest.RestHelper;
078import org.nuxeo.ecm.platform.ui.web.rest.api.URLPolicyService;
079import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
080import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
081import org.nuxeo.ecm.platform.url.DocumentViewImpl;
082import org.nuxeo.ecm.platform.url.api.DocumentView;
083import org.nuxeo.ecm.platform.url.codec.DocumentFileCodec;
084import org.nuxeo.runtime.api.Framework;
085
086/**
087 * Document model functions.
088 *
089 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
090 * @author <a href="mailto:og@nuxeo.com">Olivier Grisel</a>
091 */
092public final class DocumentModelFunctions implements LiveEditConstants {
093
094    private static final Log log = LogFactory.getLog(DocumentModelFunctions.class);
095
096    private static final String JSESSIONID = "JSESSIONID";
097
098    private static final String i18n_prefix = "%i18n";
099
100    private static final String NXEDIT_URL_VIEW_ID = "nxliveedit.faces";
101
102    private static final String NXEDIT_URL_SCHEME = "nxedit";
103
104    private static MimetypeRegistry mimetypeService;
105
106    private static TypeManager typeManagerService;
107
108    private static DirectoryService dirService;
109
110    private static LifeCycleService lifeCycleService;
111
112    // static cache of default viewId per document type shared all among
113    // threads
114    private static final Map<String, String> defaultViewCache = Collections.synchronizedMap(new HashMap<String, String>());
115
116    // Utility class.
117    private DocumentModelFunctions() {
118    }
119
120    private static DirectoryService getDirectoryService() {
121        if (dirService == null) {
122            dirService = DirectoryHelper.getDirectoryService();
123        }
124        return dirService;
125    }
126
127    private static MimetypeRegistry getMimetypeService() {
128        if (mimetypeService == null) {
129            mimetypeService = Framework.getService(MimetypeRegistry.class);
130        }
131        return mimetypeService;
132    }
133
134    private static TypeManager getTypeManager() {
135        if (typeManagerService == null) {
136            typeManagerService = Framework.getService(TypeManager.class);
137        }
138        return typeManagerService;
139    }
140
141    private static String getDefaultView(DocumentModel doc) {
142        String docType = doc.getType();
143
144        if (defaultViewCache.containsKey(docType)) {
145            return defaultViewCache.get(docType);
146        } else {
147            org.nuxeo.ecm.platform.types.Type type = getTypeManager().getType(docType);
148            if (type == null) {
149                return null;
150            }
151            String defaultView = type.getDefaultView();
152            defaultViewCache.put(docType, defaultView);
153            return defaultView;
154        }
155    }
156
157    private static LifeCycleService geLifeCycleService() {
158        if (lifeCycleService == null) {
159            lifeCycleService = NXCore.getLifeCycleService();
160            if (lifeCycleService == null) {
161                log.error("No Life Cycle service registered");
162            }
163        }
164        return lifeCycleService;
165    }
166
167    public static TypeInfo typeInfo(DocumentModel document) {
168        if (document != null) {
169            return document.getAdapter(TypeInfo.class);
170        } else {
171            return null;
172        }
173    }
174
175    public static String typeLabel(DocumentModel document) {
176        String label = "";
177        if (document != null) {
178            TypeInfo typeInfo = document.getAdapter(TypeInfo.class);
179            if (typeInfo != null) {
180                label = typeInfo.getLabel();
181            }
182        }
183        return label;
184    }
185
186    public static String typeView(DocumentModel document, String viewId) {
187        String viewValue = "";
188        if (document != null) {
189            TypeInfo typeInfo = document.getAdapter(TypeInfo.class);
190            if (typeInfo != null) {
191                viewValue = typeInfo.getView(viewId);
192            }
193        }
194        return viewValue;
195    }
196
197    public static String iconPath(DocumentModel document) {
198        String iconPath = "";
199        if (document != null) {
200            try {
201                iconPath = (String) document.getProperty("common", "icon");
202            } catch (PropertyException e) {
203                iconPath = null;
204            }
205            if (iconPath == null || iconPath.length() == 0 || document.getType().equals("Workspace")) {
206                TypeInfo typeInfo = document.getAdapter(TypeInfo.class);
207                if (typeInfo != null) {
208                    iconPath = typeInfo.getIcon();
209                }
210            }
211        }
212        return iconPath;
213    }
214
215    public static String iconExpandedPath(DocumentModel document) {
216        String iconPath = "";
217        if (document != null) {
218            try {
219                iconPath = (String) document.getProperty("common", "icon-expanded");
220            } catch (PropertyException e) {
221                iconPath = null;
222            }
223            if (iconPath == null || iconPath.length() == 0) {
224                TypeInfo typeInfo = document.getAdapter(TypeInfo.class);
225                if (typeInfo != null) {
226                    iconPath = typeInfo.getIconExpanded();
227                    // Set to default icon if expanded is not set
228                    if (iconPath == null || iconPath.equals("")) {
229                        iconPath = iconPath(document);
230                    }
231                }
232
233            }
234        }
235        return iconPath;
236    }
237
238    public static String bigIconPath(DocumentModel document) {
239        String iconPath = "";
240        if (document != null) {
241            TypeInfo typeInfo = document.getAdapter(TypeInfo.class);
242            if (typeInfo != null) {
243                iconPath = typeInfo.getBigIcon();
244            }
245        }
246        return iconPath;
247    }
248
249    public static String bigIconExpandedPath(DocumentModel document) {
250        String iconPath = "";
251        if (document != null) {
252            TypeInfo typeInfo = document.getAdapter(TypeInfo.class);
253            if (typeInfo != null) {
254                iconPath = typeInfo.getIconExpanded();
255                // Set to default icon if expanded is not set
256                if (iconPath == null || iconPath.equals("")) {
257                    iconPath = bigIconPath(document);
258                }
259            }
260        }
261        return iconPath;
262    }
263
264    public static String fileIconPath(Blob blob) {
265        String iconPath = "";
266        if (blob != null) {
267            MimetypeEntry mimeEntry = getMimetypeService().getMimetypeEntryByMimeType(blob.getMimeType());
268            if (mimeEntry != null) {
269                if (mimeEntry.getIconPath() != null) {
270                    // FIXME: above Context should find it
271                    iconPath = "/icons/" + mimeEntry.getIconPath();
272                }
273            }
274        }
275        return iconPath;
276    }
277
278    public static String titleOrId(DocumentModel document) {
279        String title = null;
280        if (document != null) {
281            try {
282                title = (String) document.getProperty("dublincore", "title");
283            } catch (PropertyException e) {
284                title = null;
285            }
286            if (title == null || title.length() == 0) {
287                // handle root document title
288                if ("/".equals(document.getPathAsString())) {
289                    title = "/";
290                } else {
291                    title = document.getId();
292                }
293            }
294        }
295
296        if (title == null) {
297            title = "<Unknown>";
298        }
299
300        if (title.startsWith(i18n_prefix)) {
301            String i18nTitle = title.substring(i18n_prefix.length());
302            FacesContext context = FacesContext.getCurrentInstance();
303            title = ComponentUtils.translate(context, i18nTitle);
304        }
305        return title;
306    }
307
308    /**
309     * @since 6.0
310     */
311    public static String titleFromId(final String documentId) {
312        final CoreSession coreSession = (CoreSession) Component.getInstance("documentManager");
313        if (StringUtils.isNotBlank(documentId)) {
314            try {
315                DocumentModel doc = coreSession.getDocument(new IdRef(documentId));
316                if (doc != null) {
317                    return titleOrId(doc);
318                }
319            } catch (DocumentNotFoundException e) {
320                log.info("Could not find document with id " + documentId);
321            }
322        }
323        return documentId;
324    }
325
326    public static boolean isDocumentModel(Object value) {
327        return value instanceof DocumentModel;
328    }
329
330    public static boolean isDirty(DocumentModel doc) {
331        if (doc == null) {
332            return false;
333        }
334        for (DocumentPart part : doc.getParts()) {
335            if (part.isDirty()) {
336                // check if dirty properties are not empty
337                Iterator<Property> props = part.getDirtyChildren();
338                if (props != null) {
339                    while (props.hasNext()) {
340                        Property prop = props.next();
341                        Serializable value = prop.getValue();
342                        if (value != null) {
343                            if (isPropertyValueDirty(value)) {
344                                return true;
345                            }
346                        }
347                    }
348                }
349            }
350        }
351        return false;
352    }
353
354    @SuppressWarnings("rawtypes")
355    protected static boolean isPropertyValueDirty(Object value) {
356        if (value instanceof String) {
357            if (!StringUtils.isBlank((String) value)) {
358                return true;
359            }
360        } else if (value instanceof List) {
361            List<?> list = (List) value;
362            if (!list.isEmpty()) {
363                return true;
364            }
365        } else if (value instanceof Collection) {
366            Collection<?> col = (Collection) value;
367            if (!col.isEmpty()) {
368                return true;
369            }
370        } else if (value instanceof Object[]) {
371            Object[] col = (Object[]) value;
372            if (col.length > 0) {
373                return true;
374            }
375        } else if (value instanceof Map) {
376            Map<?, ?> map = (Map) value;
377            if (map.isEmpty()) {
378                return true;
379            }
380            for (Object mapItem : map.values()) {
381                if (isPropertyValueDirty(mapItem)) {
382                    return true;
383                }
384            }
385        } else if (value != null) {
386            // in any other case, test if object is null (int, calendar,
387            // etc...)
388            return true;
389        }
390        return false;
391    }
392
393    public static boolean hasPermission(DocumentModel document, String permission) {
394        if (document == null) {
395            return false;
396        }
397        CoreSession session = document.getCoreSession();
398        if (session == null) {
399            session = (CoreSession) Component.getInstance("documentManager", ScopeType.CONVERSATION);
400        }
401        if (session == null) {
402            log.error("Cannot retrieve CoreSession for " + document);
403            return false;
404        }
405        boolean granted = session.hasPermission(document.getRef(), permission);
406        return granted;
407    }
408
409    /**
410     * Returns true if document can be modified.
411     * <p>
412     * A document can be modified if current user has 'Write' permission on it and document is mutable (no archived
413     * version).
414     *
415     * @param document
416     * @return true if document can be modified.
417     */
418    public static boolean canModify(DocumentModel document) {
419        if (document == null) {
420            return false;
421        }
422        return hasPermission(document, SecurityConstants.WRITE) && !document.hasFacet(FacetNames.IMMUTABLE);
423    }
424
425    /**
426     * Returns the default value for given schema field.
427     *
428     * @param schemaName the schema name
429     * @param fieldName the field name
430     * @return the default value.
431     * @deprecated use defaultValue(propertyName) instead
432     */
433    @Deprecated
434    public static Object defaultValue(String schemaName, String fieldName) {
435        Object value = null;
436        SchemaManager tm = Framework.getService(SchemaManager.class);
437        Schema schema = tm.getSchema(schemaName);
438        Field field = schema.getField(fieldName);
439        Type type = field.getType();
440        if (type.isListType()) {
441            Type itemType = ((ListType) type).getFieldType();
442            value = itemType.newInstance();
443        }
444        return value;
445    }
446
447    /**
448     * Returns the default value for given property name.
449     *
450     * @param propertyName as xpath
451     * @return the default value.
452     */
453    public static Object defaultValue(String propertyName) {
454        SchemaManager tm = Framework.getService(SchemaManager.class);
455        Field field = tm.getField(propertyName);
456        Object value = null;
457        if (field != null) {
458            Type type = field.getType();
459            if (type.isListType()) {
460                Type itemType = ((ListType) type).getFieldType();
461                value = itemType.newInstance();
462            } else if (type.isComplexType()) {
463                value = type.newInstance();
464            } else {
465                value = field.getDefaultValue();
466            }
467        }
468        return value;
469    }
470
471    /**
472     * @since 6.0
473     */
474    public static String fileUrl(String baseURL, String patternName, DocumentModel doc, String blobPropertyName,
475            String filename) {
476        if (doc == null) {
477            return null;
478        }
479        DocumentLocation docLoc = new DocumentLocationImpl(doc);
480        Map<String, String> params = new HashMap<String, String>();
481        params.put(DocumentFileCodec.FILE_PROPERTY_PATH_KEY, blobPropertyName);
482        params.put(DocumentFileCodec.FILENAME_KEY, filename);
483        DocumentView docView = new DocumentViewImpl(docLoc, null, params);
484
485        // generate url
486        URLPolicyService service = Framework.getService(URLPolicyService.class);
487        if (patternName == null) {
488            patternName = service.getDefaultPatternName();
489        }
490        return service.getUrlFromDocumentView(patternName, docView, baseURL);
491    }
492
493    public static String fileUrl(String patternName, DocumentModel doc, String blobPropertyName, String filename) {
494        return fileUrl(BaseURL.getBaseURL(), patternName, doc, blobPropertyName, filename);
495    }
496
497    public static String bigFileUrl(DocumentModel doc, String blobPropertyName, String filename) {
498        if (doc == null) {
499            return null;
500        }
501        DownloadService downloadService = Framework.getService(DownloadService.class);
502        return BaseURL.getBaseURL() + downloadService.getDownloadUrl(doc, blobPropertyName, filename);
503    }
504
505    public static String fileDescription(DocumentModel document, String blobPropertyName, String filePropertyName,
506            String filename) {
507        String fileInfo = "";
508        if (document != null) {
509            Long blobLength = null;
510            try {
511                Blob blob = (Blob) document.getPropertyValue(blobPropertyName);
512                if (blob != null) {
513                    blobLength = blob.getLength();
514                }
515            } catch (PropertyException e) {
516                // no prop by that name with that type
517            }
518            if (filename == null && filePropertyName != null) {
519                try {
520                    filename = (String) document.getPropertyValue(filePropertyName);
521                } catch (PropertyException e) {
522                    // no prop by that name with that type
523                }
524            }
525            if (blobLength != null && filename != null) {
526                fileInfo = filename + " [" + Functions.printFileSize(String.valueOf(blobLength)) + "]";
527            } else if (blobLength != null) {
528                fileInfo = "[" + Functions.printFileSize(String.valueOf(blobLength)) + "]";
529            } else if (filename != null) {
530                fileInfo = filename;
531            }
532        }
533        return fileInfo;
534    }
535
536    /**
537     * Convenient method to get the REST URL of a blob inside the <code>Files</code> schema.
538     *
539     * @param patternName
540     * @param doc The document model.
541     * @param index index of the element containing the blob. <code>index</code> starts at 0.
542     * @param filename The filename of the blob.
543     * @return the REST URL for the blob, or <code>null</code> if an error occurred.
544     */
545    public static String complexFileUrl(String patternName, DocumentModel doc, int index, String filename) {
546        return complexFileUrl(patternName, doc, "files:files", index, DEFAULT_SUB_BLOB_FIELD, filename);
547    }
548
549    /**
550     * Get the REST URL for a blob inside a list of complex type. For instance,
551     * <code>http://localhost/nuxeo/nxfile/server/docId/files:files%5B0%5D/file/image.png</code> for the blob property
552     * 'file' of the first element inside the 'files:files' list.
553     *
554     * @param patternName
555     * @param doc The document model.
556     * @param listElement Element containing a list of complex type.
557     * @param index Index of the element containing the blob inside the list. <code>index</code> starts at 0.
558     * @param blobPropertyName The property containing the blob.
559     * @param filename Filename of the blob.
560     * @return the REST URL for the blob, or <code>null</code> if an error occurred.
561     */
562    public static String complexFileUrl(String patternName, DocumentModel doc, String listElement, int index,
563            String blobPropertyName, String filename) {
564        DocumentLocation docLoc = new DocumentLocationImpl(doc);
565        Map<String, String> params = new HashMap<String, String>();
566
567        String fileProperty = getPropertyPath(listElement, index, blobPropertyName);
568
569        params.put(DocumentFileCodec.FILE_PROPERTY_PATH_KEY, fileProperty);
570        params.put(DocumentFileCodec.FILENAME_KEY, filename);
571        DocumentView docView = new DocumentViewImpl(docLoc, null, params);
572
573        // generate url
574        URLPolicyService service = Framework.getService(URLPolicyService.class);
575        if (patternName == null) {
576            patternName = service.getDefaultPatternName();
577        }
578        return service.getUrlFromDocumentView(patternName, docView, BaseURL.getBaseURL());
579    }
580
581    // TODO: add method accepting filename property name (used for edit online)
582
583    public static String documentUrl(DocumentModel doc, HttpServletRequest req) {
584        return documentUrl(null, doc, null, null, false);
585    }
586
587    public static String documentUrl(DocumentModel doc) {
588        return documentUrl(null, doc, null, null, false);
589    }
590
591    /**
592     * @deprecated since 7.3, use {@link #documentUrl(String, DocumentLocation, String, Map, boolean, String)} instead.
593     */
594    public static String documentUrl(String patternName, DocumentModel doc, String viewId,
595            Map<String, String> parameters, boolean newConversation) {
596        return documentUrl(patternName, doc, viewId, parameters, newConversation, (HttpServletRequest) null);
597    }
598
599    /**
600     * @deprecated since 7.3, use {@link #documentUrl(String, DocumentLocation, String, Map, boolean, String)} instead.
601     */
602    public static String documentUrl(String patternName, DocumentModel doc, String viewId,
603            Map<String, String> parameters, boolean newConversation, HttpServletRequest req) {
604        String baseURL = null;
605        if (req == null) {
606            baseURL = BaseURL.getBaseURL();
607        } else {
608            baseURL = BaseURL.getBaseURL(req);
609        }
610        return documentUrl(patternName, doc, viewId, parameters, newConversation, baseURL);
611    }
612
613    /**
614     * Computes a document URL.
615     *
616     * @since 7.3
617     */
618    public static String documentUrl(String patternName, DocumentModel doc, String viewId,
619            Map<String, String> parameters, boolean newConversation, String baseURL) {
620        DocumentLocation docLoc = new DocumentLocationImpl(doc);
621        if (viewId == null || viewId.length() == 0) {
622            viewId = getDefaultView(doc);
623        }
624        parameters = parameters == null ? new HashMap<String, String>() : parameters;
625
626        if (doc.isVersion()) {
627            parameters.put("version", "true");
628        }
629        return documentUrl(patternName, docLoc, viewId, parameters, newConversation, baseURL);
630    }
631
632    /**
633     * @since 5.7
634     */
635    public static String documentUrl(String patternName, DocumentLocation docLoc, String viewId,
636            Map<String, String> parameters, boolean newConversation, HttpServletRequest req) {
637        String baseURL = null;
638        if (req == null) {
639            baseURL = BaseURL.getBaseURL();
640        } else {
641            baseURL = BaseURL.getBaseURL(req);
642        }
643        return documentUrl(patternName, docLoc, viewId, parameters, newConversation, baseURL);
644    }
645
646    /**
647     * Computes a document URL.
648     *
649     * @since 7.3
650     */
651    public static String documentUrl(String patternName, DocumentLocation docLoc, String viewId,
652            Map<String, String> parameters, boolean newConversation, String baseURL) {
653        DocumentView docView = new DocumentViewImpl(docLoc, viewId, parameters);
654
655        // generate url
656        URLPolicyService service = Framework.getService(URLPolicyService.class);
657        if (patternName == null || patternName.length() == 0) {
658            patternName = service.getDefaultPatternName();
659        }
660
661        String url = service.getUrlFromDocumentView(patternName, docView, baseURL);
662
663        // pass conversation info if needed
664        if (!newConversation && url != null) {
665            url = RestHelper.addCurrentConversationParameters(url);
666        }
667
668        return url;
669    }
670
671    /**
672     * Computes an URL for a {@code repositoryName} only.
673     *
674     * @since 5.7
675     * @deprecated since 7.3, use {@link #repositoryUrl(String, String, String, Map, boolean, String)} instead.
676     */
677    public static String repositoryUrl(String patternName, String repositoryName, String viewId,
678            Map<String, String> parameters, boolean newConversation) {
679        return repositoryUrl(patternName, repositoryName, viewId, parameters, newConversation,
680                (HttpServletRequest) null);
681    }
682
683    /**
684     * Computes an URL for a {@code repositoryName} only.
685     *
686     * @since 5.7
687     * @deprecated since 7.3, use {@link #repositoryUrl(String, String, String, Map, boolean, String)} instead.
688     */
689    public static String repositoryUrl(String patternName, String repositoryName, String viewId,
690            Map<String, String> parameters, boolean newConversation, HttpServletRequest req) {
691        DocumentLocation docLoc = new DocumentLocationImpl(repositoryName, null);
692        parameters = parameters == null ? new HashMap<String, String>() : parameters;
693        return documentUrl(patternName, docLoc, viewId, parameters, newConversation, req);
694    }
695
696    /**
697     * Computes an URL for a {@code repositoryName} only.
698     *
699     * @since 7.3
700     */
701    public static String repositoryUrl(String patternName, String repositoryName, String viewId,
702            Map<String, String> parameters, boolean newConversation, String baseURL) {
703        DocumentLocation docLoc = new DocumentLocationImpl(repositoryName, null);
704        parameters = parameters == null ? new HashMap<String, String>() : parameters;
705        return documentUrl(patternName, docLoc, viewId, parameters, newConversation, baseURL);
706    }
707
708    protected static void addQueryParameter(StringBuilder sb, String name, String value, boolean isFirst)
709            {
710        if (isFirst) {
711            sb.append("?");
712        } else {
713            sb.append("&");
714        }
715        if (value == null) {
716            return;
717        }
718        sb.append(name);
719        sb.append("=");
720        try {
721            sb.append(URLEncoder.encode(value, URL_ENCODE_CHARSET));
722        } catch (UnsupportedEncodingException e) {
723            throw new NuxeoException("Could not encode URL parameter: " + name + "=" + value, e);
724        }
725    }
726
727    /**
728     * Build the nxedit URL for the "edit existing document" use case for a document using the file:content field as
729     * Blob holder
730     *
731     * @return the encoded URL string
732     */
733    public static String liveEditUrl(DocumentModel doc) {
734        return liveEditUrl(doc, DEFAULT_SCHEMA, DEFAULT_BLOB_FIELD, DEFAULT_FILENAME_FIELD);
735    }
736
737    /**
738     * Build the nxedit URL for the "edit existing document" use case
739     *
740     * @return the encoded URL string
741     */
742    public static String liveEditUrl(DocumentModel doc, String schemaName, String blobFieldName,
743            String filenameFieldName) {
744        if (doc == null) {
745            return ""; // JSF DebugUtil.printTree may call this
746        }
747        StringBuilder queryParamBuilder = new StringBuilder();
748        addQueryParameter(queryParamBuilder, ACTION, ACTION_EDIT_DOCUMENT, true);
749        addQueryParameter(queryParamBuilder, REPO_ID, doc.getRepositoryName(), false);
750        addQueryParameter(queryParamBuilder, DOC_REF, doc.getRef().toString(), false);
751        if (schemaName == null || "".equals(schemaName)) {
752            // try to extract it from blob field name
753            schemaName = DocumentModelUtils.getSchemaName(blobFieldName);
754            blobFieldName = DocumentModelUtils.getFieldName(blobFieldName);
755            filenameFieldName = DocumentModelUtils.getFieldName(filenameFieldName);
756        }
757        addQueryParameter(queryParamBuilder, SCHEMA, schemaName, false);
758        addQueryParameter(queryParamBuilder, BLOB_FIELD, blobFieldName, false);
759        addQueryParameter(queryParamBuilder, FILENAME_FIELD, filenameFieldName, false);
760        return buildNxEditUrl(queryParamBuilder.toString());
761    }
762
763    /**
764     * Build the nxedit URL for the "edit existing document" use case
765     *
766     * @return the encoded URL string
767     */
768    public static String complexLiveEditUrl(DocumentModel doc, String listPropertyName, int index,
769            String blobPropertyName, String filenamePropertyName) {
770
771        StringBuilder queryParamBuilder = new StringBuilder();
772        addQueryParameter(queryParamBuilder, ACTION, ACTION_EDIT_DOCUMENT, true);
773        addQueryParameter(queryParamBuilder, REPO_ID, doc.getRepositoryName(), false);
774        addQueryParameter(queryParamBuilder, DOC_REF, doc.getRef().toString(), false);
775        addQueryParameter(queryParamBuilder, BLOB_PROPERTY_NAME,
776                getPropertyPath(listPropertyName, index, blobPropertyName), false);
777        addQueryParameter(queryParamBuilder, FILENAME_PROPERTY_NAME,
778                getPropertyPath(listPropertyName, index, filenamePropertyName), false);
779        return buildNxEditUrl(queryParamBuilder.toString());
780    }
781
782    /**
783     * Build the nxedit URL for the "create new document" use case with a document using the file:content field as Blob
784     * holder
785     *
786     * @param mimetype the mime type of the newly created document
787     * @return the encoded URL string
788     */
789    public static String liveCreateUrl(String mimetype) {
790        return liveCreateUrl(mimetype, DEFAULT_DOCTYPE, DEFAULT_SCHEMA, DEFAULT_BLOB_FIELD, DEFAULT_FILENAME_FIELD);
791    }
792
793    /**
794     * Build the nxedit URL for the "create new document" use case
795     *
796     * @param mimetype the mime type of the newly created document
797     * @param docType the document type of the document to create
798     * @param schemaName the schema of the blob to hold the new attachment
799     * @param blobFieldName the field name of the blob to hold the new attachment
800     * @param filenameFieldName the field name of the filename of the new attachment
801     * @return the encoded URL string
802     */
803    public static String liveCreateUrl(String mimetype, String docType, String schemaName, String blobFieldName,
804            String filenameFieldName) {
805
806        StringBuilder queryParamBuilder = new StringBuilder();
807        addQueryParameter(queryParamBuilder, ACTION, ACTION_CREATE_DOCUMENT, true);
808        addQueryParameter(queryParamBuilder, MIMETYPE, mimetype, false);
809        addQueryParameter(queryParamBuilder, SCHEMA, schemaName, false);
810        addQueryParameter(queryParamBuilder, BLOB_FIELD, blobFieldName, false);
811        addQueryParameter(queryParamBuilder, FILENAME_FIELD, filenameFieldName, false);
812        addQueryParameter(queryParamBuilder, DOC_TYPE, docType, false);
813        return buildNxEditUrl(queryParamBuilder.toString());
814    }
815
816    /**
817     * Build the nxedit URL for the "create new document from template" use case with "File" doc type and "file" schema
818     *
819     * @param template the document holding the blob to be used as template
820     * @return the encoded URL string
821     */
822    public static String liveCreateFromTemplateUrl(DocumentModel template) {
823        return liveCreateFromTemplateUrl(template, DEFAULT_SCHEMA, DEFAULT_BLOB_FIELD, DEFAULT_DOCTYPE, DEFAULT_SCHEMA,
824                DEFAULT_BLOB_FIELD, DEFAULT_FILENAME_FIELD);
825    }
826
827    /**
828     * Build the nxedit URL for the "create new document from template" use case
829     *
830     * @param template the document holding the blob to be used as template
831     * @param templateSchemaName the schema of the blob holding the template
832     * @param templateBlobFieldName the field name of the blob holding the template
833     * @param docType the document type of the new document to create
834     * @param schemaName the schema of the new blob to be saved as attachment
835     * @param blobFieldName the field name of the new blob to be saved as attachment
836     * @param filenameFieldName the field name of the filename of the attachment
837     * @return the encoded URL string
838     */
839    public static String liveCreateFromTemplateUrl(DocumentModel template, String templateSchemaName,
840            String templateBlobFieldName, String docType, String schemaName, String blobFieldName,
841            String filenameFieldName) {
842
843        StringBuilder queryParamBuilder = new StringBuilder();
844        addQueryParameter(queryParamBuilder, ACTION, ACTION_CREATE_DOCUMENT_FROM_TEMPLATE, true);
845        addQueryParameter(queryParamBuilder, TEMPLATE_REPO_ID, template.getRepositoryName(), false);
846        addQueryParameter(queryParamBuilder, TEMPLATE_DOC_REF, template.getRef().toString(), false);
847        addQueryParameter(queryParamBuilder, TEMPLATE_SCHEMA, templateSchemaName, false);
848        addQueryParameter(queryParamBuilder, TEMPLATE_BLOB_FIELD, templateBlobFieldName, false);
849        addQueryParameter(queryParamBuilder, SCHEMA, schemaName, false);
850        addQueryParameter(queryParamBuilder, BLOB_FIELD, blobFieldName, false);
851        addQueryParameter(queryParamBuilder, FILENAME_FIELD, filenameFieldName, false);
852        addQueryParameter(queryParamBuilder, DOC_TYPE, docType, false);
853        return buildNxEditUrl(queryParamBuilder.toString());
854    }
855
856    private static String buildNxEditUrl(String queryParameters) {
857        FacesContext context = FacesContext.getCurrentInstance();
858        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
859
860        // build the URL prefix by concatenating nxedit: scheme with the
861        // http:// or https:// base URL from the current request context and
862        // the LiveEditBoostrapHelper JSF view
863        StringBuilder nxeditUrlBuilder = new StringBuilder(NXEDIT_URL_SCHEME);
864        nxeditUrlBuilder.append(":");
865        nxeditUrlBuilder.append(BaseURL.getBaseURL(request));
866        nxeditUrlBuilder.append(NXEDIT_URL_VIEW_ID);
867
868        // add the query parameters them selves
869        nxeditUrlBuilder.append(queryParameters);
870
871        // add seam conversation and JSESSION ids
872        addQueryParameter(nxeditUrlBuilder, Manager.instance().getConversationIdParameter(),
873                Manager.instance().getCurrentConversationId(), false);
874        addQueryParameter(nxeditUrlBuilder, JSESSIONID, extractJSessionId(request), false);
875        return nxeditUrlBuilder.toString();
876    }
877
878    /**
879     * Extract the current JSESSIONID value from the request context
880     *
881     * @param request the current HttpServletRequest request
882     * @return the current JSESSIONID string
883     */
884    public static String extractJSessionId(HttpServletRequest request) {
885        if (request.getSession() != null) {
886            return request.getSession().getId();
887        }
888        if (request.getCookies() != null) {
889            for (Cookie cookie : request.getCookies()) {
890                if (cookie.getName().equalsIgnoreCase("jsessionid")) {
891                    return cookie.getValue();
892                }
893            }
894        }
895        return null;
896    }
897
898    /**
899     * Returns the label for given directory and id.
900     *
901     * @param directoryName the directory name
902     * @param id the label id
903     * @return the label.
904     * @throws DirectoryException
905     * @deprecated use {@link DirectoryFunctions#getDirectoryEntry(String, String)}
906     */
907    @Deprecated
908    public static String getLabelFromId(String directoryName, String id) throws DirectoryException {
909        if (id == null) {
910            return "";
911        }
912        try (Session directory = getDirectoryService().open(directoryName)) {
913            // XXX hack, directory entries have only one datamodel
914            DocumentModel documentModel = directory.getEntry(id);
915            String schemaName = documentModel.getSchemas()[0];
916            return (String) documentModel.getProperty(schemaName, "label");
917        } catch (PropertyException e) {
918            return "";
919        }
920    }
921
922    public static String getPropertyPath(String listPropertyName, int index, String subPropertyName) {
923        return listPropertyName + "/" + index + "/" + subPropertyName;
924    }
925
926    /**
927     * Returns all the available transitions given the current state.
928     *
929     * @param lifeCycleName the Life Cycle name
930     * @param currentState the state from which the transitions should start
931     * @since 5.4.2
932     */
933    public static Collection<String> getAvailableLifeCycleTransitions(String lifeCycleName, String currentState)
934            throws LifeCycleException {
935        LifeCycle lf = geLifeCycleService().getLifeCycleByName(lifeCycleName);
936        return lf.getAllowedStateTransitionsFrom(currentState);
937    }
938
939    /**
940     * Reset default view cache.
941     *
942     * @since 5.8
943     */
944    public static void resetDefaultViewCache() {
945        defaultViewCache.clear();
946    }
947
948}