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