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