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 *     Sean Radford
017 *
018 * $Id: ComponentUtils.java 28924 2008-01-10 14:04:05Z sfermigier $
019 */
020
021package org.nuxeo.ecm.platform.ui.web.util;
022
023import java.io.File;
024import java.io.IOException;
025import java.io.Serializable;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.List;
029import java.util.Locale;
030import java.util.Map;
031
032import javax.el.ValueExpression;
033import javax.faces.application.FacesMessage;
034import javax.faces.component.UIComponent;
035import javax.faces.component.UISelectItems;
036import javax.faces.component.UISelectMany;
037import javax.faces.context.ExternalContext;
038import javax.faces.context.FacesContext;
039import javax.faces.model.SelectItem;
040import javax.servlet.http.HttpServletRequest;
041import javax.servlet.http.HttpServletResponse;
042
043import org.apache.commons.logging.Log;
044import org.apache.commons.logging.LogFactory;
045import org.nuxeo.common.utils.i18n.I18NUtils;
046import org.nuxeo.ecm.core.api.Blob;
047import org.nuxeo.ecm.core.api.Blobs;
048import org.nuxeo.ecm.core.api.DocumentModel;
049import org.nuxeo.ecm.core.io.download.DownloadService;
050import org.nuxeo.ecm.platform.ui.web.component.list.UIEditableList;
051import org.nuxeo.ecm.platform.web.common.ServletHelper;
052import org.nuxeo.ecm.platform.web.common.exceptionhandling.ExceptionHelper;
053import org.nuxeo.runtime.api.Framework;
054
055/**
056 * Generic component helper methods.
057 *
058 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
059 */
060public final class ComponentUtils {
061
062    public static final String WHITE_SPACE_CHARACTER = "&#x0020;";
063
064    private static final Log log = LogFactory.getLog(ComponentUtils.class);
065
066    private static final String VH_HEADER = "nuxeo-virtual-host";
067
068    private static final String VH_PARAM = "nuxeo.virtual.host";
069
070    public static final String FORCE_NO_CACHE_ON_MSIE = "org.nuxeo.download.force.nocache.msie";
071
072    // Utility class.
073    private ComponentUtils() {
074    }
075
076    /**
077     * Calls a component encodeBegin/encodeChildren/encodeEnd methods.
078     */
079    public static void encodeComponent(FacesContext context, UIComponent component) throws IOException {
080        component.encodeBegin(context);
081        component.encodeChildren(context);
082        component.encodeEnd(context);
083    }
084
085    /**
086     * Helper method meant to be called in the component constructor.
087     * <p>
088     * When adding sub components dynamically, the tree fetching could be a problem so all possible sub components must
089     * be added.
090     * <p>
091     * Since 6.0, does not mark component as not rendered anymore, calls
092     * {@link #hookSubComponent(FacesContext, UIComponent, UIComponent, String)} directly.
093     *
094     * @param parent
095     * @param child
096     * @param facetName facet name to put the child in.
097     */
098    public static void initiateSubComponent(UIComponent parent, String facetName, UIComponent child) {
099        parent.getFacets().put(facetName, child);
100        hookSubComponent(null, parent, child, facetName);
101    }
102
103    /**
104     * Add a sub component to a UI component.
105     * <p>
106     * Since 6.0, does not the set the component as rendered anymore.
107     *
108     * @param context
109     * @param parent
110     * @param child
111     * @param defaultChildId
112     * @return child comp
113     */
114    public static UIComponent hookSubComponent(FacesContext context, UIComponent parent, UIComponent child,
115            String defaultChildId) {
116        // build a valid id using the parent id so that it's found everytime.
117        String childId = child.getId();
118        if (defaultChildId != null) {
119            // override with default
120            childId = defaultChildId;
121        }
122        // make sure it's set
123        if (childId == null) {
124            childId = context.getViewRoot().createUniqueId();
125        }
126        // reset client id
127        child.setId(childId);
128        child.setParent(parent);
129        return child;
130    }
131
132    /**
133     * Copies attributes and value expressions with given name from parent component to child component.
134     */
135    public static void copyValues(UIComponent parent, UIComponent child, String[] valueNames) {
136        Map<String, Object> parentAttributes = parent.getAttributes();
137        Map<String, Object> childAttributes = child.getAttributes();
138        for (String name : valueNames) {
139            // attributes
140            if (parentAttributes.containsKey(name)) {
141                childAttributes.put(name, parentAttributes.get(name));
142            }
143            // value expressions
144            ValueExpression ve = parent.getValueExpression(name);
145            if (ve != null) {
146                child.setValueExpression(name, ve);
147            }
148        }
149    }
150
151    public static void copyLinkValues(UIComponent parent, UIComponent child) {
152        String[] valueNames = { "accesskey", "charset", "coords", "dir", "disabled", "hreflang", "lang", "onblur",
153                "onclick", "ondblclick", "onfocus", "onkeydown", "onkeypress", "onkeyup", "onmousedown", "onmousemove",
154                "onmouseout", "onmouseover", "onmouseup", "rel", "rev", "shape", "style", "styleClass", "tabindex",
155                "target", "title", "type" };
156        copyValues(parent, child, valueNames);
157    }
158
159    public static Object getAttributeValue(UIComponent component, String attributeName, Object defaultValue) {
160        Object value = component.getAttributes().get(attributeName);
161        if (value == null) {
162            value = defaultValue;
163        }
164        return value;
165    }
166
167    public static Object getAttributeOrExpressionValue(FacesContext context, UIComponent component,
168            String attributeName, Object defaultValue) {
169        Object value = component.getAttributes().get(attributeName);
170        if (value == null) {
171            ValueExpression schemaExpr = component.getValueExpression(attributeName);
172            value = schemaExpr.getValue(context.getELContext());
173        }
174        if (value == null) {
175            value = defaultValue;
176        }
177        return value;
178    }
179
180    /**
181     * Downloads a blob and sends it to the requesting user, in the JSF current context.
182     *
183     * @param doc the document, if available
184     * @param xpath the blob's xpath or blobholder index, if available
185     * @param blob the blob, if already fetched
186     * @param filename the filename to use
187     * @param reason the download reason
188     *
189     * @since 7.3
190     */
191   public static void download(DocumentModel doc, String xpath, Blob blob, String filename, String reason) {
192       download(doc, xpath, blob, filename, reason, null);
193   }
194
195    /**
196     * Downloads a blob and sends it to the requesting user, in the JSF current context.
197     *
198     * @param doc the document, if available
199     * @param xpath the blob's xpath or blobholder index, if available
200     * @param blob the blob, if already fetched
201     * @param filename the filename to use
202     * @param reason the download reason
203     * @param extendedInfos an optional map of extended informations to log
204     *
205     * @since 7.3
206     */
207    public static void download(DocumentModel doc, String xpath, Blob blob, String filename, String reason,
208            Map<String, Serializable> extendedInfos) {
209        FacesContext facesContext = FacesContext.getCurrentInstance();
210        if (facesContext.getResponseComplete()) {
211            // nothing can be written, an error was probably already sent. don't bother
212            log.debug("Cannot send " + filename + ", response already complete");
213            return;
214        }
215        if (facesContext.getPartialViewContext().isAjaxRequest()) {
216            // do not perform download in an ajax request
217            return;
218        }
219        ExternalContext externalContext = facesContext.getExternalContext();
220        HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
221        HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
222        try {
223            DownloadService downloadService = Framework.getService(DownloadService.class);
224            downloadService.downloadBlob(request, response, doc, xpath, blob, filename, reason, extendedInfos);
225        } catch (IOException e) {
226            log.error("Error while downloading the file: " + filename, e);
227        } finally {
228            facesContext.responseComplete();
229        }
230    }
231
232    public static String downloadFile(File file, String filename, String reason) throws IOException {
233        Blob blob = Blobs.createBlob(file);
234        download(null, null, blob, filename, reason);
235        return null;
236    }
237
238    /**
239     * @deprecated since 7.3, use {@link #downloadFile(Blob, String)} instead
240     */
241    @Deprecated
242    public static String download(FacesContext faces, Blob blob, String filename) {
243        download(null, null, blob, filename, "download");
244        return null;
245    }
246
247    /**
248     * @deprecated since 7.3, use {@link #downloadFile(File, String)} instead
249     */
250    @Deprecated
251    public static String downloadFile(FacesContext faces, String filename, File file) throws IOException {
252        return downloadFile(file, filename, null);
253    }
254
255    protected static boolean forceNoCacheOnMSIE() {
256        // see NXP-7759
257        return Framework.isBooleanPropertyTrue(FORCE_NO_CACHE_ON_MSIE);
258    }
259
260    /**
261     * Internet Explorer file downloads over SSL do not work with certain HTTP cache control headers
262     * <p>
263     * See http://support.microsoft.com/kb/323308/
264     * <p>
265     * What is not mentioned in the above Knowledge Base is that "Pragma: no-cache" also breaks download in MSIE over
266     * SSL
267     */
268    private static void addCacheControlHeaders(HttpServletRequest request, HttpServletResponse response) {
269        String userAgent = request.getHeader("User-Agent");
270        boolean secure = request.isSecure();
271        if (!secure) {
272            String nvh = request.getHeader(VH_HEADER);
273            if (nvh == null) {
274                nvh = Framework.getProperty(VH_PARAM);
275            }
276            if (nvh != null) {
277                secure = nvh.startsWith("https");
278            }
279        }
280        log.debug("User-Agent: " + userAgent);
281        log.debug("secure: " + secure);
282        if (userAgent.contains("MSIE") && (secure || forceNoCacheOnMSIE())) {
283            log.debug("Setting \"Cache-Control: max-age=15, must-revalidate\"");
284            response.setHeader("Cache-Control", "max-age=15, must-revalidate");
285        } else {
286            log.debug("Setting \"Cache-Control: private\" and \"Pragma: no-cache\"");
287            response.setHeader("Cache-Control", "private, must-revalidate");
288            response.setHeader("Pragma", "no-cache");
289            response.setDateHeader("Expires", 0);
290        }
291    }
292
293    // hook translation passing faces context
294
295    public static String translate(FacesContext context, String messageId) {
296        return translate(context, messageId, (Object[]) null);
297    }
298
299    public static String translate(FacesContext context, String messageId, Object... params) {
300        String bundleName = context.getApplication().getMessageBundle();
301        Locale locale = context.getViewRoot().getLocale();
302        return I18NUtils.getMessageString(bundleName, messageId, evaluateParams(context, params), locale);
303    }
304
305    public static void addErrorMessage(FacesContext context, UIComponent component, String message) {
306        addErrorMessage(context, component, message, null);
307    }
308
309    public static void addErrorMessage(FacesContext context, UIComponent component, String message, Object[] params) {
310        String bundleName = context.getApplication().getMessageBundle();
311        Locale locale = context.getViewRoot().getLocale();
312        message = I18NUtils.getMessageString(bundleName, message, evaluateParams(context, params), locale);
313        FacesMessage msg = new FacesMessage(message);
314        msg.setSeverity(FacesMessage.SEVERITY_ERROR);
315        context.addMessage(component.getClientId(context), msg);
316    }
317
318    /**
319     * Evaluates parameters to pass to translation methods if they are value expressions.
320     *
321     * @since 5.7
322     */
323    protected static Object[] evaluateParams(FacesContext context, Object[] params) {
324        if (params == null) {
325            return null;
326        }
327        Object[] res = new Object[params.length];
328        for (int i = 0; i < params.length; i++) {
329            Object val = params[i];
330            if (val instanceof String && ComponentTagUtils.isValueReference((String) val)) {
331                ValueExpression ve = context.getApplication().getExpressionFactory().createValueExpression(
332                        context.getELContext(), (String) val, Object.class);
333                res[i] = ve.getValue(context.getELContext());
334            } else {
335                res[i] = val;
336            }
337        }
338        return res;
339    }
340
341    /**
342     * Gets the base naming container from anchor.
343     * <p>
344     * Gets out of suggestion box as it's a naming container and we can't get components out of it with a relative path
345     * => take above first found container.
346     *
347     * @since 5.3.1
348     */
349    public static UIComponent getBase(UIComponent anchor) {
350        // init base to given component in case there's no naming container for it
351        UIComponent base = anchor;
352        UIComponent container = anchor.getNamingContainer();
353        if (container != null) {
354            UIComponent supContainer = container.getNamingContainer();
355            if (supContainer != null) {
356                container = supContainer;
357            }
358        }
359        if (container != null) {
360            base = container;
361        }
362        if (log.isDebugEnabled()) {
363            log.debug(String.format("Resolved base '%s' for anchor '%s'", base.getId(), anchor.getId()));
364        }
365        return base;
366    }
367
368    /**
369     * Returns the component specified by the {@code componentId} parameter from the {@code base} component.
370     * <p>
371     * Does not throw any exception if the component is not found, returns {@code null} instead.
372     *
373     * @since 5.4
374     */
375    @SuppressWarnings("unchecked")
376    public static <T> T getComponent(UIComponent base, String componentId, Class<T> expectedComponentClass) {
377        if (componentId == null) {
378            log.error("Cannot retrieve component with a null id");
379            return null;
380        }
381        UIComponent component = ComponentRenderUtils.getComponent(base, componentId);
382        if (component == null) {
383            log.error("Could not find component with id: " + componentId);
384        } else {
385            try {
386                return (T) component;
387            } catch (ClassCastException e) {
388                log.error(String.format(
389                        "Invalid component with id %s: %s, expected a " + "component with interface %s", componentId,
390                        component, expectedComponentClass));
391            }
392        }
393        return null;
394    }
395
396    static void clearTargetList(UIEditableList targetList) {
397        int rc = targetList.getRowCount();
398        for (int i = 0; i < rc; i++) {
399            targetList.removeValue(0);
400        }
401    }
402
403    static void addToTargetList(UIEditableList targetList, SelectItem[] items) {
404        for (int i = 0; i < items.length; i++) {
405            targetList.addValue(items[i].getValue());
406        }
407    }
408
409    /**
410     * Move items up inside the target select
411     */
412    public static void shiftItemsUp(UISelectMany targetSelect, UISelectItems targetItems,
413            UIEditableList hiddenTargetList) {
414        String[] selected = (String[]) targetSelect.getSelectedValues();
415        SelectItem[] all = (SelectItem[]) targetItems.getValue();
416        if (selected == null) {
417            // nothing to do
418            return;
419        }
420        shiftUp(selected, all);
421        targetItems.setValue(all);
422        clearTargetList(hiddenTargetList);
423        addToTargetList(hiddenTargetList, all);
424    }
425
426    public static void shiftItemsDown(UISelectMany targetSelect, UISelectItems targetItems,
427            UIEditableList hiddenTargetList) {
428        String[] selected = (String[]) targetSelect.getSelectedValues();
429        SelectItem[] all = (SelectItem[]) targetItems.getValue();
430        if (selected == null) {
431            // nothing to do
432            return;
433        }
434        shiftDown(selected, all);
435        targetItems.setValue(all);
436        clearTargetList(hiddenTargetList);
437        addToTargetList(hiddenTargetList, all);
438    }
439
440    public static void shiftItemsFirst(UISelectMany targetSelect, UISelectItems targetItems,
441            UIEditableList hiddenTargetList) {
442        String[] selected = (String[]) targetSelect.getSelectedValues();
443        SelectItem[] all = (SelectItem[]) targetItems.getValue();
444        if (selected == null) {
445            // nothing to do
446            return;
447        }
448        all = shiftFirst(selected, all);
449        targetItems.setValue(all);
450        clearTargetList(hiddenTargetList);
451        addToTargetList(hiddenTargetList, all);
452    }
453
454    public static void shiftItemsLast(UISelectMany targetSelect, UISelectItems targetItems,
455            UIEditableList hiddenTargetList) {
456        String[] selected = (String[]) targetSelect.getSelectedValues();
457        SelectItem[] all = (SelectItem[]) targetItems.getValue();
458        if (selected == null) {
459            // nothing to do
460            return;
461        }
462        all = shiftLast(selected, all);
463        targetItems.setValue(all);
464        clearTargetList(hiddenTargetList);
465        addToTargetList(hiddenTargetList, all);
466    }
467
468    /**
469     * Make a new SelectItem[] with items whose ids belong to selected first, preserving inner ordering of selected and
470     * its complement in all.
471     * <p>
472     * Again this assumes that selected is an ordered sub-list of all
473     * </p>
474     *
475     * @param selected ids of selected items
476     * @param all
477     * @return
478     */
479    static SelectItem[] shiftFirst(String[] selected, SelectItem[] all) {
480        SelectItem[] res = new SelectItem[all.length];
481        int sl = selected.length;
482        int i = 0;
483        int j = sl;
484        for (SelectItem item : all) {
485            if (i < sl && item.getValue().toString().equals(selected[i])) {
486                res[i++] = item;
487            } else {
488                res[j++] = item;
489            }
490        }
491        return res;
492    }
493
494    /**
495     * Make a new SelectItem[] with items whose ids belong to selected last, preserving inner ordering of selected and
496     * its complement in all.
497     * <p>
498     * Again this assumes that selected is an ordered sub-list of all
499     * </p>
500     *
501     * @param selected ids of selected items
502     * @param all
503     * @return
504     */
505    static SelectItem[] shiftLast(String[] selected, SelectItem[] all) {
506        SelectItem[] res = new SelectItem[all.length];
507        int sl = selected.length;
508        int cut = all.length - sl;
509        int i = 0;
510        int j = 0;
511        for (SelectItem item : all) {
512            if (i < sl && item.getValue().toString().equals(selected[i])) {
513                res[cut + i++] = item;
514            } else {
515                res[j++] = item;
516            }
517        }
518        return res;
519    }
520
521    static void swap(Object[] ar, int i, int j) {
522        Object t = ar[i];
523        ar[i] = ar[j];
524        ar[j] = t;
525    }
526
527    static void shiftUp(String[] selected, SelectItem[] all) {
528        int pos = -1;
529        for (int i = 0; i < selected.length; i++) {
530            String s = selected[i];
531            // "pos" is the index of previous "s"
532            int previous = pos;
533            while (!all[++pos].getValue().equals(s)) {
534            }
535            // now current "s" is at "pos" index
536            if (pos > previous + 1) {
537                swap(all, pos, --pos);
538            }
539        }
540    }
541
542    static void shiftDown(String[] selected, SelectItem[] all) {
543        int pos = all.length;
544        for (int i = selected.length - 1; i >= 0; i--) {
545            String s = selected[i];
546            // "pos" is the index of previous "s"
547            int previous = pos;
548            while (!all[--pos].getValue().equals(s)) {
549            }
550            // now current "s" is at "pos" index
551            if (pos < previous - 1) {
552                swap(all, pos, ++pos);
553            }
554        }
555    }
556
557    /**
558     * Move items from components to others.
559     */
560    public static void moveItems(UISelectMany sourceSelect, UISelectItems sourceItems, UISelectItems targetItems,
561            UIEditableList hiddenTargetList, boolean setTargetIds) {
562        String[] selected = (String[]) sourceSelect.getSelectedValues();
563        if (selected == null) {
564            // nothing to do
565            return;
566        }
567        List<String> selectedList = Arrays.asList(selected);
568
569        SelectItem[] all = (SelectItem[]) sourceItems.getValue();
570        List<SelectItem> toMove = new ArrayList<SelectItem>();
571        List<SelectItem> toKeep = new ArrayList<SelectItem>();
572        List<String> hiddenIds = new ArrayList<String>();
573        if (all != null) {
574            for (SelectItem item : all) {
575                String itemId = item.getValue().toString();
576                if (selectedList.contains(itemId)) {
577                    toMove.add(item);
578                } else {
579                    toKeep.add(item);
580                    if (!setTargetIds) {
581                        hiddenIds.add(itemId);
582                    }
583                }
584            }
585        }
586        // reset left values
587        sourceItems.setValue(toKeep.toArray(new SelectItem[] {}));
588        sourceSelect.setSelectedValues(new Object[0]);
589
590        // change right values
591        List<SelectItem> newSelectItems = new ArrayList<SelectItem>();
592        SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue();
593        if (oldSelectItems == null) {
594            newSelectItems.addAll(toMove);
595        } else {
596            newSelectItems.addAll(Arrays.asList(oldSelectItems));
597            List<String> oldIds = new ArrayList<String>();
598            for (SelectItem oldItem : oldSelectItems) {
599                String id = oldItem.getValue().toString();
600                oldIds.add(id);
601            }
602            if (setTargetIds) {
603                hiddenIds.addAll(0, oldIds);
604            }
605            for (SelectItem toMoveItem : toMove) {
606                String id = toMoveItem.getValue().toString();
607                if (!oldIds.contains(id)) {
608                    newSelectItems.add(toMoveItem);
609                    if (setTargetIds) {
610                        hiddenIds.add(id);
611                    }
612                }
613            }
614        }
615        targetItems.setValue(newSelectItems.toArray(new SelectItem[] {}));
616
617        // update hidden values
618        int numValues = hiddenTargetList.getRowCount();
619        if (numValues > 0) {
620            for (int i = numValues - 1; i > -1; i--) {
621                hiddenTargetList.removeValue(i);
622            }
623        }
624        for (String newHiddenValue : hiddenIds) {
625            hiddenTargetList.addValue(newHiddenValue);
626        }
627    }
628
629    /**
630     * Move items from components to others.
631     */
632    public static void moveAllItems(UISelectItems sourceItems, UISelectItems targetItems,
633            UIEditableList hiddenTargetList, boolean setTargetIds) {
634        SelectItem[] all = (SelectItem[]) sourceItems.getValue();
635        List<SelectItem> toMove = new ArrayList<SelectItem>();
636        List<SelectItem> toKeep = new ArrayList<SelectItem>();
637        List<String> hiddenIds = new ArrayList<String>();
638        if (all != null) {
639            for (SelectItem item : all) {
640                if (!item.isDisabled()) {
641                    toMove.add(item);
642                } else {
643                    toKeep.add(item);
644                }
645            }
646        }
647        // reset left values
648        sourceItems.setValue(toKeep.toArray(new SelectItem[] {}));
649
650        // change right values
651        List<SelectItem> newSelectItems = new ArrayList<SelectItem>();
652        SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue();
653        if (oldSelectItems == null) {
654            newSelectItems.addAll(toMove);
655        } else {
656            newSelectItems.addAll(Arrays.asList(oldSelectItems));
657            List<String> oldIds = new ArrayList<String>();
658            for (SelectItem oldItem : oldSelectItems) {
659                String id = oldItem.getValue().toString();
660                oldIds.add(id);
661            }
662            if (setTargetIds) {
663                hiddenIds.addAll(0, oldIds);
664            }
665            for (SelectItem toMoveItem : toMove) {
666                String id = toMoveItem.getValue().toString();
667                if (!oldIds.contains(id)) {
668                    newSelectItems.add(toMoveItem);
669                    if (setTargetIds) {
670                        hiddenIds.add(id);
671                    }
672                }
673            }
674        }
675        targetItems.setValue(newSelectItems.toArray(new SelectItem[] {}));
676
677        // update hidden values
678        int numValues = hiddenTargetList.getRowCount();
679        if (numValues > 0) {
680            for (int i = numValues - 1; i > -1; i--) {
681                hiddenTargetList.removeValue(i);
682            }
683        }
684        for (String newHiddenValue : hiddenIds) {
685            hiddenTargetList.addValue(newHiddenValue);
686        }
687    }
688
689}