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        UIComponent base = anchor;
351        UIComponent container = anchor.getNamingContainer();
352        if (container != null) {
353            UIComponent supContainer = container.getNamingContainer();
354            if (supContainer != null) {
355                container = supContainer;
356            }
357        }
358        if (log.isDebugEnabled()) {
359            log.debug(String.format("Resolved base '%s' for anchor '%s'", base.getId(), anchor.getId()));
360        }
361        return base;
362    }
363
364    /**
365     * Returns the component specified by the {@code componentId} parameter from the {@code base} component.
366     * <p>
367     * Does not throw any exception if the component is not found, returns {@code null} instead.
368     *
369     * @since 5.4
370     */
371    @SuppressWarnings("unchecked")
372    public static <T> T getComponent(UIComponent base, String componentId, Class<T> expectedComponentClass) {
373        if (componentId == null) {
374            log.error("Cannot retrieve component with a null id");
375            return null;
376        }
377        UIComponent component = ComponentRenderUtils.getComponent(base, componentId);
378        if (component == null) {
379            log.error("Could not find component with id: " + componentId);
380        } else {
381            try {
382                return (T) component;
383            } catch (ClassCastException e) {
384                log.error(String.format(
385                        "Invalid component with id %s: %s, expected a " + "component with interface %s", componentId,
386                        component, expectedComponentClass));
387            }
388        }
389        return null;
390    }
391
392    static void clearTargetList(UIEditableList targetList) {
393        int rc = targetList.getRowCount();
394        for (int i = 0; i < rc; i++) {
395            targetList.removeValue(0);
396        }
397    }
398
399    static void addToTargetList(UIEditableList targetList, SelectItem[] items) {
400        for (int i = 0; i < items.length; i++) {
401            targetList.addValue(items[i].getValue());
402        }
403    }
404
405    /**
406     * Move items up inside the target select
407     */
408    public static void shiftItemsUp(UISelectMany targetSelect, UISelectItems targetItems,
409            UIEditableList hiddenTargetList) {
410        String[] selected = (String[]) targetSelect.getSelectedValues();
411        SelectItem[] all = (SelectItem[]) targetItems.getValue();
412        if (selected == null) {
413            // nothing to do
414            return;
415        }
416        shiftUp(selected, all);
417        targetItems.setValue(all);
418        clearTargetList(hiddenTargetList);
419        addToTargetList(hiddenTargetList, all);
420    }
421
422    public static void shiftItemsDown(UISelectMany targetSelect, UISelectItems targetItems,
423            UIEditableList hiddenTargetList) {
424        String[] selected = (String[]) targetSelect.getSelectedValues();
425        SelectItem[] all = (SelectItem[]) targetItems.getValue();
426        if (selected == null) {
427            // nothing to do
428            return;
429        }
430        shiftDown(selected, all);
431        targetItems.setValue(all);
432        clearTargetList(hiddenTargetList);
433        addToTargetList(hiddenTargetList, all);
434    }
435
436    public static void shiftItemsFirst(UISelectMany targetSelect, UISelectItems targetItems,
437            UIEditableList hiddenTargetList) {
438        String[] selected = (String[]) targetSelect.getSelectedValues();
439        SelectItem[] all = (SelectItem[]) targetItems.getValue();
440        if (selected == null) {
441            // nothing to do
442            return;
443        }
444        all = shiftFirst(selected, all);
445        targetItems.setValue(all);
446        clearTargetList(hiddenTargetList);
447        addToTargetList(hiddenTargetList, all);
448    }
449
450    public static void shiftItemsLast(UISelectMany targetSelect, UISelectItems targetItems,
451            UIEditableList hiddenTargetList) {
452        String[] selected = (String[]) targetSelect.getSelectedValues();
453        SelectItem[] all = (SelectItem[]) targetItems.getValue();
454        if (selected == null) {
455            // nothing to do
456            return;
457        }
458        all = shiftLast(selected, all);
459        targetItems.setValue(all);
460        clearTargetList(hiddenTargetList);
461        addToTargetList(hiddenTargetList, all);
462    }
463
464    /**
465     * Make a new SelectItem[] with items whose ids belong to selected first, preserving inner ordering of selected and
466     * its complement in all.
467     * <p>
468     * Again this assumes that selected is an ordered sub-list of all
469     * </p>
470     *
471     * @param selected ids of selected items
472     * @param all
473     * @return
474     */
475    static SelectItem[] shiftFirst(String[] selected, SelectItem[] all) {
476        SelectItem[] res = new SelectItem[all.length];
477        int sl = selected.length;
478        int i = 0;
479        int j = sl;
480        for (SelectItem item : all) {
481            if (i < sl && item.getValue().toString().equals(selected[i])) {
482                res[i++] = item;
483            } else {
484                res[j++] = item;
485            }
486        }
487        return res;
488    }
489
490    /**
491     * Make a new SelectItem[] with items whose ids belong to selected last, preserving inner ordering of selected and
492     * its complement in all.
493     * <p>
494     * Again this assumes that selected is an ordered sub-list of all
495     * </p>
496     *
497     * @param selected ids of selected items
498     * @param all
499     * @return
500     */
501    static SelectItem[] shiftLast(String[] selected, SelectItem[] all) {
502        SelectItem[] res = new SelectItem[all.length];
503        int sl = selected.length;
504        int cut = all.length - sl;
505        int i = 0;
506        int j = 0;
507        for (SelectItem item : all) {
508            if (i < sl && item.getValue().toString().equals(selected[i])) {
509                res[cut + i++] = item;
510            } else {
511                res[j++] = item;
512            }
513        }
514        return res;
515    }
516
517    static void swap(Object[] ar, int i, int j) {
518        Object t = ar[i];
519        ar[i] = ar[j];
520        ar[j] = t;
521    }
522
523    static void shiftUp(String[] selected, SelectItem[] all) {
524        int pos = -1;
525        for (int i = 0; i < selected.length; i++) {
526            String s = selected[i];
527            // "pos" is the index of previous "s"
528            int previous = pos;
529            while (!all[++pos].getValue().equals(s)) {
530            }
531            // now current "s" is at "pos" index
532            if (pos > previous + 1) {
533                swap(all, pos, --pos);
534            }
535        }
536    }
537
538    static void shiftDown(String[] selected, SelectItem[] all) {
539        int pos = all.length;
540        for (int i = selected.length - 1; i >= 0; i--) {
541            String s = selected[i];
542            // "pos" is the index of previous "s"
543            int previous = pos;
544            while (!all[--pos].getValue().equals(s)) {
545            }
546            // now current "s" is at "pos" index
547            if (pos < previous - 1) {
548                swap(all, pos, ++pos);
549            }
550        }
551    }
552
553    /**
554     * Move items from components to others.
555     */
556    public static void moveItems(UISelectMany sourceSelect, UISelectItems sourceItems, UISelectItems targetItems,
557            UIEditableList hiddenTargetList, boolean setTargetIds) {
558        String[] selected = (String[]) sourceSelect.getSelectedValues();
559        if (selected == null) {
560            // nothing to do
561            return;
562        }
563        List<String> selectedList = Arrays.asList(selected);
564
565        SelectItem[] all = (SelectItem[]) sourceItems.getValue();
566        List<SelectItem> toMove = new ArrayList<SelectItem>();
567        List<SelectItem> toKeep = new ArrayList<SelectItem>();
568        List<String> hiddenIds = new ArrayList<String>();
569        if (all != null) {
570            for (SelectItem item : all) {
571                String itemId = item.getValue().toString();
572                if (selectedList.contains(itemId)) {
573                    toMove.add(item);
574                } else {
575                    toKeep.add(item);
576                    if (!setTargetIds) {
577                        hiddenIds.add(itemId);
578                    }
579                }
580            }
581        }
582        // reset left values
583        sourceItems.setValue(toKeep.toArray(new SelectItem[] {}));
584        sourceSelect.setSelectedValues(new Object[0]);
585
586        // change right values
587        List<SelectItem> newSelectItems = new ArrayList<SelectItem>();
588        SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue();
589        if (oldSelectItems == null) {
590            newSelectItems.addAll(toMove);
591        } else {
592            newSelectItems.addAll(Arrays.asList(oldSelectItems));
593            List<String> oldIds = new ArrayList<String>();
594            for (SelectItem oldItem : oldSelectItems) {
595                String id = oldItem.getValue().toString();
596                oldIds.add(id);
597            }
598            if (setTargetIds) {
599                hiddenIds.addAll(0, oldIds);
600            }
601            for (SelectItem toMoveItem : toMove) {
602                String id = toMoveItem.getValue().toString();
603                if (!oldIds.contains(id)) {
604                    newSelectItems.add(toMoveItem);
605                    if (setTargetIds) {
606                        hiddenIds.add(id);
607                    }
608                }
609            }
610        }
611        targetItems.setValue(newSelectItems.toArray(new SelectItem[] {}));
612
613        // update hidden values
614        int numValues = hiddenTargetList.getRowCount();
615        if (numValues > 0) {
616            for (int i = numValues - 1; i > -1; i--) {
617                hiddenTargetList.removeValue(i);
618            }
619        }
620        for (String newHiddenValue : hiddenIds) {
621            hiddenTargetList.addValue(newHiddenValue);
622        }
623    }
624
625    /**
626     * Move items from components to others.
627     */
628    public static void moveAllItems(UISelectItems sourceItems, UISelectItems targetItems,
629            UIEditableList hiddenTargetList, boolean setTargetIds) {
630        SelectItem[] all = (SelectItem[]) sourceItems.getValue();
631        List<SelectItem> toMove = new ArrayList<SelectItem>();
632        List<SelectItem> toKeep = new ArrayList<SelectItem>();
633        List<String> hiddenIds = new ArrayList<String>();
634        if (all != null) {
635            for (SelectItem item : all) {
636                if (!item.isDisabled()) {
637                    toMove.add(item);
638                } else {
639                    toKeep.add(item);
640                }
641            }
642        }
643        // reset left values
644        sourceItems.setValue(toKeep.toArray(new SelectItem[] {}));
645
646        // change right values
647        List<SelectItem> newSelectItems = new ArrayList<SelectItem>();
648        SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue();
649        if (oldSelectItems == null) {
650            newSelectItems.addAll(toMove);
651        } else {
652            newSelectItems.addAll(Arrays.asList(oldSelectItems));
653            List<String> oldIds = new ArrayList<String>();
654            for (SelectItem oldItem : oldSelectItems) {
655                String id = oldItem.getValue().toString();
656                oldIds.add(id);
657            }
658            if (setTargetIds) {
659                hiddenIds.addAll(0, oldIds);
660            }
661            for (SelectItem toMoveItem : toMove) {
662                String id = toMoveItem.getValue().toString();
663                if (!oldIds.contains(id)) {
664                    newSelectItems.add(toMoveItem);
665                    if (setTargetIds) {
666                        hiddenIds.add(id);
667                    }
668                }
669            }
670        }
671        targetItems.setValue(newSelectItems.toArray(new SelectItem[] {}));
672
673        // update hidden values
674        int numValues = hiddenTargetList.getRowCount();
675        if (numValues > 0) {
676            for (int i = numValues - 1; i > -1; i--) {
677                hiddenTargetList.removeValue(i);
678            }
679        }
680        for (String newHiddenValue : hiddenIds) {
681            hiddenTargetList.addValue(newHiddenValue);
682        }
683    }
684
685}