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