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