001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui;
029
030import org.opencms.ade.galleries.CmsSiteSelectorOptionBuilder;
031import org.opencms.ade.galleries.shared.CmsSiteSelectorOption;
032import org.opencms.configuration.preferences.CmsLanguagePreference;
033import org.opencms.db.CmsUserSettings;
034import org.opencms.file.CmsGroup;
035import org.opencms.file.CmsObject;
036import org.opencms.file.CmsProject;
037import org.opencms.file.CmsUser;
038import org.opencms.file.types.A_CmsResourceTypeFolderBase;
039import org.opencms.file.types.CmsResourceTypeXmlContent;
040import org.opencms.file.types.I_CmsResourceType;
041import org.opencms.i18n.CmsEncoder;
042import org.opencms.i18n.CmsMessages;
043import org.opencms.i18n.I_CmsMessageBundle;
044import org.opencms.main.CmsException;
045import org.opencms.main.CmsLog;
046import org.opencms.main.OpenCms;
047import org.opencms.security.CmsOrganizationalUnit;
048import org.opencms.security.CmsRole;
049import org.opencms.security.I_CmsPrincipal;
050import org.opencms.ui.apps.CmsAppWorkplaceUi;
051import org.opencms.ui.apps.Messages;
052import org.opencms.ui.apps.user.CmsOUHandler;
053import org.opencms.ui.components.OpenCmsTheme;
054import org.opencms.ui.contextmenu.CmsContextMenu;
055import org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry;
056import org.opencms.util.CmsFileUtil;
057import org.opencms.util.CmsMacroResolver;
058import org.opencms.util.CmsStringUtil;
059import org.opencms.util.CmsUUID;
060import org.opencms.workplace.CmsWorkplace;
061import org.opencms.workplace.CmsWorkplaceMessages;
062import org.opencms.workplace.explorer.CmsExplorerTypeSettings;
063import org.opencms.workplace.explorer.CmsResourceUtil;
064
065import java.io.ByteArrayInputStream;
066import java.io.IOException;
067import java.io.InputStream;
068import java.io.UnsupportedEncodingException;
069import java.util.ArrayList;
070import java.util.Arrays;
071import java.util.Collection;
072import java.util.Collections;
073import java.util.HashSet;
074import java.util.Iterator;
075import java.util.LinkedHashMap;
076import java.util.List;
077import java.util.Locale;
078import java.util.Map;
079import java.util.Map.Entry;
080
081import javax.servlet.http.HttpServletRequest;
082
083import org.apache.commons.lang3.ClassUtils;
084import org.apache.commons.logging.Log;
085
086import com.google.common.base.Function;
087import com.google.common.base.Joiner;
088import com.google.common.base.Predicate;
089import com.google.common.collect.Lists;
090import com.vaadin.server.ErrorMessage;
091import com.vaadin.server.ExternalResource;
092import com.vaadin.server.FontIcon;
093import com.vaadin.server.Resource;
094import com.vaadin.server.VaadinService;
095import com.vaadin.shared.MouseEventDetails.MouseButton;
096import com.vaadin.shared.Version;
097import com.vaadin.ui.AbstractComponent;
098import com.vaadin.ui.Alignment;
099import com.vaadin.ui.Button;
100import com.vaadin.ui.Button.ClickEvent;
101import com.vaadin.ui.Button.ClickListener;
102import com.vaadin.ui.Component;
103import com.vaadin.ui.ComponentContainer;
104import com.vaadin.ui.HasComponents;
105import com.vaadin.ui.JavaScript;
106import com.vaadin.ui.Panel;
107import com.vaadin.ui.SingleComponentContainer;
108import com.vaadin.ui.TextField;
109import com.vaadin.ui.UI;
110import com.vaadin.ui.Window;
111import com.vaadin.ui.declarative.Design;
112import com.vaadin.ui.themes.ValoTheme;
113import com.vaadin.v7.data.Container;
114import com.vaadin.v7.data.Container.Filter;
115import com.vaadin.v7.data.Item;
116import com.vaadin.v7.data.util.IndexedContainer;
117import com.vaadin.v7.event.ItemClickEvent;
118import com.vaadin.v7.shared.ui.combobox.FilteringMode;
119import com.vaadin.v7.ui.AbstractField;
120import com.vaadin.v7.ui.ComboBox;
121import com.vaadin.v7.ui.Label;
122import com.vaadin.v7.ui.OptionGroup;
123import com.vaadin.v7.ui.Table;
124import com.vaadin.v7.ui.VerticalLayout;
125
126/**
127 * Vaadin utility functions.<p>
128 *
129 */
130@SuppressWarnings("deprecation")
131public final class CmsVaadinUtils {
132
133    /**
134     * Helper class for building option groups.<p>
135     */
136    public static class OptionGroupBuilder {
137
138        /** The option group being built. */
139        private OptionGroup m_optionGroup = new OptionGroup();
140
141        /**
142         * Adds an option.<p>
143         *
144         * @param key the option key
145         * @param text the option text
146         *
147         * @return this instance
148         */
149        public OptionGroupBuilder add(String key, String text) {
150
151            m_optionGroup.addItem(key);
152            m_optionGroup.setItemCaption(key, text);
153            return this;
154        }
155
156        /**
157         * Returns the option group.<p>
158         *
159         * @return the option group
160         */
161        public OptionGroup build() {
162
163            return m_optionGroup;
164        }
165
166        /**
167         * Adds horizontal style to option group.<p>
168         *
169         * @return this instance
170         */
171        public OptionGroupBuilder horizontal() {
172
173            m_optionGroup.addStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL);
174            return this;
175        }
176    }
177
178    /** Container property ids. */
179    public static enum PropertyId {
180        /** The caption id. */
181        caption,
182        /** The icon id. */
183        icon,
184        /** The is folder id. */
185        isFolder,
186        /** The is XML content id. */
187        isXmlContent
188    }
189
190    /** Container filter for the resource type container to show not folder types only. */
191    public static final Filter FILTER_NO_FOLDERS = new Filter() {
192
193        private static final long serialVersionUID = 1L;
194
195        public boolean appliesToProperty(Object propertyId) {
196
197            return PropertyId.isFolder.equals(propertyId);
198        }
199
200        public boolean passesFilter(Object itemId, Item item) throws UnsupportedOperationException {
201
202            return !((Boolean)item.getItemProperty(PropertyId.isFolder).getValue()).booleanValue();
203        }
204    };
205
206    /** Container filter for the resource type container to show XML content types only. */
207    public static final Filter FILTER_XML_CONTENTS = new Filter() {
208
209        private static final long serialVersionUID = 1L;
210
211        public boolean appliesToProperty(Object propertyId) {
212
213            return PropertyId.isXmlContent.equals(propertyId);
214        }
215
216        public boolean passesFilter(Object itemId, Item item) throws UnsupportedOperationException {
217
218            return ((Boolean)item.getItemProperty(PropertyId.isXmlContent).getValue()).booleanValue();
219        }
220    };
221
222    /** The combo box label item property id. */
223    public static final String PROPERTY_LABEL = "label";
224
225    /** The combo box value item property id. */
226    public static final String PROPERTY_VALUE = "value";
227
228    /** The Vaadin bootstrap script, with some macros to be dynamically replaced later. */
229    protected static final String BOOTSTRAP_SCRIPT = "vaadin.initApplication(\"%(elementId)\", {\n"
230        + "        \"browserDetailsUrl\": \"%(vaadinServlet)\",\n"
231        + "        \"serviceUrl\": \"%(vaadinServlet)\",\n"
232        + "        \"widgetset\": \"org.opencms.ui.WidgetSet\",\n"
233        + "        \"theme\": \"opencms\",\n"
234        + "        \"versionInfo\": {\"vaadinVersion\": \"%(vaadinVersion)\"},\n"
235        + "        \"vaadinDir\": \"%(vaadinDir)\",\n"
236        + "        \"heartbeatInterval\": 30,\n"
237        + "        \"debug\": false,\n"
238        + "        \"standalone\": false,\n"
239        + "        \"authErrMsg\": {\n"
240        + "            \"message\": \"Take note of any unsaved data, \"+\n"
241        + "                       \"and <u>click here<\\/u> to continue.\",\n"
242        + "            \"caption\": \"Authentication problem\"\n"
243        + "        },\n"
244        + "        \"comErrMsg\": {\n"
245        + "            \"message\": \"Take note of any unsaved data, \"+\n"
246        + "                       \"and <u>click here<\\/u> to continue.\",\n"
247        + "            \"caption\": \"Communication problem\"\n"
248        + "        },\n"
249        + "        \"sessExpMsg\": {\n"
250        + "            \"message\": \"Take note of any unsaved data, \"+\n"
251        + "                       \"and <u>click here<\\/u> to continue.\",\n"
252        + "            \"caption\": \"Session Expired\"\n"
253        + "        }\n"
254        + "    });";
255
256    /** The logger of this class. */
257    private static final Log LOG = CmsLog.getLog(CmsVaadinUtils.class);
258
259    /**
260     * Hidden default constructor for utility class.<p>
261     */
262    private CmsVaadinUtils() {
263
264    }
265
266    /**
267     * Builds a container for use in combo boxes from a map of key/value pairs, where the keys are options and the values are captions.<p>
268     *
269     * @param captionProperty the property name to use for captions
270     * @param map the map
271     * @return the new container
272     */
273    public static IndexedContainer buildContainerFromMap(String captionProperty, Map<String, String> map) {
274
275        IndexedContainer container = new IndexedContainer();
276        for (Map.Entry<String, String> entry : map.entrySet()) {
277            container.addItem(entry.getKey()).getItemProperty(captionProperty).setValue(entry.getValue());
278        }
279        return container;
280    }
281
282    /**
283     * Centers the parent window of given component.<p>
284     *
285     * @param component Component as child of window
286     */
287    public static void centerWindow(Component component) {
288
289        Window window = getWindow(component);
290        if (window != null) {
291            window.center();
292        }
293    }
294
295    /**
296     * Closes the window containing the given component.
297     *
298     * @param component a component
299     */
300    public static void closeWindow(Component component) {
301
302        Window window = getWindow(component);
303        if (window != null) {
304            window.close();
305        }
306    }
307
308    /**
309     * Creates a click listener which calls a Runnable when activated.<p>
310     *
311     * @param action the Runnable to execute on a click
312     *
313     * @return the click listener
314     */
315    public static Button.ClickListener createClickListener(final Runnable action) {
316
317        return new Button.ClickListener() {
318
319            /** Serial version id. */
320            private static final long serialVersionUID = 1L;
321
322            public void buttonClick(ClickEvent event) {
323
324                action.run();
325            }
326        };
327    }
328
329    /**
330     * Simple context menu handler for multi-select tables.
331     *
332     * @param table the table
333     * @param menu the table's context menu
334     * @param event the click event
335     * @param entries the context menu entries
336     */
337    @SuppressWarnings("unchecked")
338    public static <T> void defaultHandleContextMenuForMultiselect(
339        Table table,
340        CmsContextMenu menu,
341        ItemClickEvent event,
342        List<I_CmsSimpleContextMenuEntry<Collection<T>>> entries) {
343
344        if (!event.isCtrlKey() && !event.isShiftKey()) {
345            if (event.getButton().equals(MouseButton.RIGHT)) {
346                Collection<T> oldValue = ((Collection<T>)table.getValue());
347                if (oldValue.isEmpty() || !oldValue.contains(event.getItemId())) {
348                    table.setValue(new HashSet<Object>(Arrays.asList(event.getItemId())));
349                }
350                Collection<T> selection = (Collection<T>)table.getValue();
351                menu.setEntries(entries, selection);
352                menu.openForTable(event, table);
353            }
354        }
355
356    }
357
358    /**
359     * Reads the content of an input stream into a string (using UTF-8 encoding), performs a function on the string, and returns the result
360     * again as an input stream.<p>
361     *
362     * @param stream the stream producing the input data
363     * @param transformation the function to apply to the input
364     *
365     * @return the stream producing the transformed input data
366     */
367    public static InputStream filterUtf8ResourceStream(InputStream stream, Function<String, String> transformation) {
368
369        try {
370            byte[] streamData = CmsFileUtil.readFully(stream);
371            String dataAsString = new String(streamData, "UTF-8");
372            byte[] transformedData = transformation.apply(dataAsString).getBytes("UTF-8");
373            return new ByteArrayInputStream(transformedData);
374        } catch (UnsupportedEncodingException e) {
375            LOG.error(e.getLocalizedMessage(), e);
376            return null;
377        } catch (IOException e) {
378            LOG.error(e.getLocalizedMessage(), e);
379            throw new RuntimeException(e);
380        }
381    }
382
383    /**
384     * Get all groups with blacklist.<p>
385     *
386     * @param cms CmsObject
387     * @param ouFqn ou name
388     * @param propCaption property
389     * @param propIcon property for icon
390     * @param propOu organizational unit
391     * @param blackList blacklist
392     * @param iconProvider the icon provider
393     * @return indexed container
394     */
395    public static IndexedContainer getAvailableGroupsContainerWithout(
396        CmsObject cms,
397        String ouFqn,
398        String propCaption,
399        String propIcon,
400        String propOu,
401        List<CmsGroup> blackList,
402        java.util.function.Function<CmsGroup, CmsCssIcon> iconProvider) {
403
404        if (blackList == null) {
405            blackList = new ArrayList<CmsGroup>();
406        }
407        IndexedContainer res = new IndexedContainer();
408        res.addContainerProperty(propCaption, String.class, "");
409        res.addContainerProperty(propOu, String.class, "");
410        if (propIcon != null) {
411            res.addContainerProperty(propIcon, CmsCssIcon.class, null);
412        }
413        try {
414            for (CmsGroup group : OpenCms.getRoleManager().getManageableGroups(cms, ouFqn, true)) {
415                if (!blackList.contains(group)) {
416                    Item item = res.addItem(group);
417                    if (item == null) {
418                        continue;
419                    }
420                    if (iconProvider != null) {
421                        item.getItemProperty(propIcon).setValue(iconProvider.apply(group));
422                    }
423                    item.getItemProperty(propCaption).setValue(group.getSimpleName());
424                    item.getItemProperty(propOu).setValue(group.getOuFqn());
425                }
426            }
427
428        } catch (CmsException e) {
429            LOG.error("Unable to read groups", e);
430        }
431        return res;
432    }
433
434    /**
435     * Returns the available projects.<p>
436     *
437     * @param cms the CMS context
438     *
439     * @return the available projects
440     */
441    public static List<CmsProject> getAvailableProjects(CmsObject cms) {
442
443        // get all project information
444        List<CmsProject> allProjects;
445        try {
446            String ouFqn = "";
447            CmsUserSettings settings = new CmsUserSettings(cms);
448            if (!settings.getListAllProjects()) {
449                ouFqn = cms.getRequestContext().getCurrentUser().getOuFqn();
450            }
451            allProjects = new ArrayList<CmsProject>(
452                OpenCms.getOrgUnitManager().getAllAccessibleProjects(cms, ouFqn, settings.getListAllProjects()));
453            Iterator<CmsProject> itProjects = allProjects.iterator();
454            while (itProjects.hasNext()) {
455                CmsProject prj = itProjects.next();
456                if (prj.isHiddenFromSelector()) {
457                    itProjects.remove();
458                }
459            }
460        } catch (CmsException e) {
461            // should usually never happen
462            LOG.error(e.getLocalizedMessage(), e);
463            allProjects = Collections.emptyList();
464        }
465        return allProjects;
466    }
467
468    /**
469     * Builds an IndexedContainer containing the sites selectable by the current user.<p>
470     *
471     * @param cms the CMS context
472     * @param captionPropertyName the name of the property used to store captions
473     *
474     * @return the container with the available sites
475     */
476    public static IndexedContainer getAvailableSitesContainer(CmsObject cms, String captionPropertyName) {
477
478        IndexedContainer availableSites = new IndexedContainer();
479        availableSites.addContainerProperty(captionPropertyName, String.class, null);
480        for (Map.Entry<String, String> entry : getAvailableSitesMap(cms).entrySet()) {
481            Item siteItem = availableSites.addItem(entry.getKey());
482            siteItem.getItemProperty(captionPropertyName).setValue(entry.getValue());
483        }
484        return availableSites;
485    }
486
487    /**
488     * Gets available sites as a LinkedHashMap, with site roots as keys and site labels as values.
489     *
490     * @param cms the current CMS context
491     * @return the map of available sites
492     */
493    public static LinkedHashMap<String, String> getAvailableSitesMap(CmsObject cms) {
494
495        CmsSiteSelectorOptionBuilder optBuilder = new CmsSiteSelectorOptionBuilder(cms);
496        optBuilder.addNormalSites(true, (new CmsUserSettings(cms)).getStartFolder());
497        optBuilder.addSharedSite();
498        LinkedHashMap<String, String> result = new LinkedHashMap<String, String>();
499        for (CmsSiteSelectorOption option : optBuilder.getOptions()) {
500            result.put(option.getSiteRoot(), option.getMessage());
501        }
502        String currentSiteRoot = cms.getRequestContext().getSiteRoot();
503        if (!result.containsKey(currentSiteRoot)) {
504            result.put(currentSiteRoot, currentSiteRoot);
505        }
506        return result;
507    }
508
509    /**
510     * Returns the Javascript code to use for initializing a Vaadin UI.<p>
511     *
512     * @param cms the CMS context
513     * @param elementId the id of the DOM element in which to initialize the UI
514     * @param servicePath the UI servlet path
515     * @return the Javascript code to initialize Vaadin
516     *
517     * @throws Exception if something goes wrong
518     */
519    public static String getBootstrapScript(CmsObject cms, String elementId, String servicePath) throws Exception {
520
521        String script = BOOTSTRAP_SCRIPT;
522        CmsMacroResolver resolver = new CmsMacroResolver();
523        String context = OpenCms.getSystemInfo().getContextPath();
524        String vaadinDir = CmsStringUtil.joinPaths(context, "VAADIN/");
525        String vaadinVersion = Version.getFullVersion();
526        String vaadinServlet = CmsStringUtil.joinPaths(context, servicePath);
527        String vaadinBootstrap = CmsStringUtil.joinPaths(context, "VAADIN/vaadinBootstrap.js");
528        resolver.addMacro("vaadinDir", vaadinDir);
529        resolver.addMacro("vaadinVersion", vaadinVersion);
530        resolver.addMacro("elementId", elementId);
531        resolver.addMacro("vaadinServlet", vaadinServlet);
532        resolver.addMacro("vaadinBootstrap", vaadinBootstrap);
533        script = resolver.resolveMacros(script);
534        return script;
535
536    }
537
538    /**
539     * Returns the path to the design template file of the given component.<p>
540     *
541     * @param component the component
542     *
543     * @return the path
544     */
545    public static String getDefaultDesignPath(Component component) {
546
547        String className = component.getClass().getName();
548        String designPath = className.replace(".", "/") + ".html";
549        return designPath;
550    }
551
552    /**
553     * Gets container with alls groups of a certain user.
554     *
555     * @param cms cmsobject
556     * @param user to find groups for
557     * @param caption caption property
558     * @param iconProp property
559     * @param ou ou
560     * @param propStatus status property
561     * @param iconProvider the icon provider
562     * @return Indexed Container
563     */
564    public static IndexedContainer getGroupsOfUser(
565        CmsObject cms,
566        CmsUser user,
567        String caption,
568        String iconProp,
569        String ou,
570        String propStatus,
571        Function<CmsGroup, CmsCssIcon> iconProvider) {
572
573        IndexedContainer container = new IndexedContainer();
574        container.addContainerProperty(caption, String.class, "");
575        container.addContainerProperty(ou, String.class, "");
576        container.addContainerProperty(propStatus, Boolean.class, Boolean.valueOf(true));
577        if (iconProvider != null) {
578            container.addContainerProperty(iconProp, CmsCssIcon.class, null);
579        }
580        try {
581            for (CmsGroup group : cms.getGroupsOfUser(user.getName(), true)) {
582                Item item = container.addItem(group);
583                item.getItemProperty(caption).setValue(group.getSimpleName());
584                item.getItemProperty(ou).setValue(group.getOuFqn());
585                if (iconProvider != null) {
586                    item.getItemProperty(iconProp).setValue(iconProvider.apply(group));
587                }
588            }
589        } catch (CmsException e) {
590            LOG.error("Unable to read groups from user", e);
591        }
592        return container;
593    }
594
595    /**
596     * Creates a layout with info panel.<p>
597     *
598     * @param messageString Message to be displayed
599     * @return layout
600     */
601    public static VerticalLayout getInfoLayout(String messageString) {
602
603        VerticalLayout ret = new VerticalLayout();
604        ret.setMargin(true);
605        ret.addStyleName("o-center");
606        ret.setWidth("100%");
607        VerticalLayout inner = new VerticalLayout();
608        inner.addStyleName("o-workplace-maxwidth");
609        Panel panel = new Panel();
610        panel.setWidth("100%");
611
612        Label label = new Label(CmsVaadinUtils.getMessageText(messageString));
613        label.addStyleName("o-report");
614        panel.setContent(label);
615
616        inner.addComponent(panel);
617        ret.addComponent(inner);
618        return ret;
619    }
620
621    /**
622     * Get container with languages.<p>
623     *
624     * @param captionPropertyName name
625     * @return indexed container
626     */
627    public static IndexedContainer getLanguageContainer(String captionPropertyName) {
628
629        IndexedContainer result = new IndexedContainer();
630        result.addContainerProperty(captionPropertyName, String.class, "");
631
632        Iterator<Locale> itLocales = OpenCms.getLocaleManager().getAvailableLocales().iterator();
633        while (itLocales.hasNext()) {
634            Locale locale = itLocales.next();
635            Item item = result.addItem(locale);
636            item.getItemProperty(captionPropertyName).setValue(locale.getDisplayName(A_CmsUI.get().getLocale()));
637        }
638
639        return result;
640
641    }
642
643    /**
644     * Gets the message for the current locale and the given key and arguments.<p>
645     *
646     * @param messages the messages instance
647     * @param key the message key
648     * @param args the message arguments
649     *
650     * @return the message text for the current locale
651     */
652    public static String getMessageText(I_CmsMessageBundle messages, String key, Object... args) {
653
654        return messages.getBundle(A_CmsUI.get().getLocale()).key(key, args);
655    }
656
657    /**
658     * Gets the workplace message for the current locale and the given key and arguments.<p>
659     *
660     * @param key the message key
661     * @param args the message arguments
662     *
663     * @return the message text for the current locale
664     */
665    public static String getMessageText(String key, Object... args) {
666
667        return getWpMessagesForCurrentLocale().key(key, args);
668    }
669
670    /**
671     * Creates the ComboBox for OU selection.<p>
672     * @param cms CmsObject
673     * @param baseOu OU
674     * @param log Logger object
675     *
676     * @return ComboBox
677     */
678    public static ComboBox getOUComboBox(CmsObject cms, String baseOu, Log log) {
679
680        return getOUComboBox(cms, baseOu, log, true);
681    }
682
683    /**
684     * Creates the ComboBox for OU selection.<p>
685     * @param cms CmsObject
686     * @param baseOu OU
687     * @param log Logger object
688     * @param includeWebOU include webou?
689     *
690     * @return ComboBox
691     */
692    public static ComboBox getOUComboBox(CmsObject cms, String baseOu, Log log, boolean includeWebOU) {
693
694        ComboBox combo = null;
695        try {
696            IndexedContainer container = new IndexedContainer();
697            container.addContainerProperty("desc", String.class, "");
698            for (String ou : CmsOUHandler.getManagableOUs(cms)) {
699                if (includeWebOU | !OpenCms.getOrgUnitManager().readOrganizationalUnit(cms, ou).hasFlagWebuser()) {
700                    Item item = container.addItem(ou);
701                    if (ou == "") {
702                        CmsOrganizationalUnit root = OpenCms.getOrgUnitManager().readOrganizationalUnit(cms, "");
703                        item.getItemProperty("desc").setValue(root.getDisplayName(A_CmsUI.get().getLocale()));
704                    } else {
705                        item.getItemProperty("desc").setValue(
706                            OpenCms.getOrgUnitManager().readOrganizationalUnit(cms, ou).getDisplayName(
707                                A_CmsUI.get().getLocale()));
708                    }
709                }
710            }
711            combo = new ComboBox(null, container);
712            combo.setTextInputAllowed(true);
713            combo.setNullSelectionAllowed(false);
714            combo.setWidth("379px");
715            combo.setInputPrompt(
716                Messages.get().getBundle(UI.getCurrent().getLocale()).key(Messages.GUI_EXPLORER_CLICK_TO_EDIT_0));
717            combo.setItemCaptionPropertyId("desc");
718
719            combo.setFilteringMode(FilteringMode.CONTAINS);
720
721            combo.select(baseOu);
722
723        } catch (CmsException e) {
724            if (log != null) {
725                log.error("Unable to read OU", e);
726            }
727        }
728        return combo;
729    }
730
731    /**
732     * Gives item id from path.<p>
733     *
734     * @param cnt to be used
735     * @param path to obtain item id from
736     * @return item id
737     */
738    public static String getPathItemId(Container cnt, String path) {
739
740        for (String id : Arrays.asList(path, CmsFileUtil.toggleTrailingSeparator(path))) {
741            if (cnt.containsId(id)) {
742                return id;
743            }
744        }
745        return null;
746    }
747
748    /**
749     * Get container for principal.
750     *
751     * @param cms cmsobject
752     * @param list of principals
753     * @param captionID caption id
754     * @param descID description id
755     * @param iconID icon id
756     * @param ouID ou id
757     * @param icon icon
758     * @param iconList iconlist
759     * @return indexedcontainer
760     */
761    public static IndexedContainer getPrincipalContainer(
762        CmsObject cms,
763        List<? extends I_CmsPrincipal> list,
764        String captionID,
765        String descID,
766        String iconID,
767        String ouID,
768        String icon,
769        List<FontIcon> iconList) {
770
771        IndexedContainer res = new IndexedContainer();
772
773        res.addContainerProperty(captionID, String.class, "");
774        res.addContainerProperty(ouID, String.class, "");
775        res.addContainerProperty(iconID, FontIcon.class, new CmsCssIcon(icon));
776        if (descID != null) {
777            res.addContainerProperty(descID, String.class, "");
778        }
779
780        for (I_CmsPrincipal principal : list) {
781
782            Item item = res.addItem(principal);
783            String name = principal.getSimpleName();
784            if (principal instanceof CmsUser) {
785                CmsUser user = (CmsUser)principal;
786                List<String> nameComponents = new ArrayList<>();
787                for (String nameComponent : Arrays.asList(user.getFirstname(), user.getLastname())) {
788                    if (!CmsStringUtil.isEmptyOrWhitespaceOnly(nameComponent)) {
789                        nameComponents.add(nameComponent);
790                    }
791                }
792                String fullName = Joiner.on(' ').join(nameComponents);
793                if (!CmsStringUtil.isEmpty(fullName)) {
794                    name = name + " (" + fullName + ")";
795                }
796            }
797            item.getItemProperty(captionID).setValue(name);
798            item.getItemProperty(ouID).setValue(principal.getOuFqn());
799            if (descID != null) {
800                String desc = principal.getDescription(A_CmsUI.get().getLocale());
801                item.getItemProperty(descID).setValue(desc);
802            }
803        }
804
805        for (int i = 0; i < iconList.size(); i++) {
806            res.getItem(res.getIdByIndex(i)).getItemProperty(iconID).setValue(iconList.get(i));
807        }
808
809        return res;
810    }
811
812    /**
813     * Returns the selectable projects container.<p>
814     *
815     * @param cms the CMS context
816     * @param captionPropertyName the name of the property used to store captions
817     *
818     * @return the projects container
819     */
820    public static IndexedContainer getProjectsContainer(CmsObject cms, String captionPropertyName) {
821
822        IndexedContainer result = new IndexedContainer();
823        result.addContainerProperty(captionPropertyName, String.class, null);
824        for (Map.Entry<CmsUUID, String> entry : getProjectsMap(cms).entrySet()) {
825            Item projectItem = result.addItem(entry.getKey());
826            projectItem.getItemProperty(captionPropertyName).setValue(entry.getValue());
827        }
828        return result;
829    }
830
831    /**
832     * Gets the available projects for the current user as a map, wth project ids as keys and project names as values.
833     *
834     * @param cms the current CMS context
835     * @return the map of projects
836     */
837    public static LinkedHashMap<CmsUUID, String> getProjectsMap(CmsObject cms) {
838
839        Locale locale = A_CmsUI.get().getLocale();
840        List<CmsProject> projects = getAvailableProjects(cms);
841        boolean isSingleOu = isSingleOu(projects);
842        LinkedHashMap<CmsUUID, String> result = new LinkedHashMap<>();
843        for (CmsProject project : projects) {
844            String projectName = project.getSimpleName();
845            if (!isSingleOu && !project.isOnlineProject()) {
846                try {
847                    projectName = projectName
848                        + " - "
849                        + OpenCms.getOrgUnitManager().readOrganizationalUnit(cms, project.getOuFqn()).getDisplayName(
850                            locale);
851                } catch (CmsException e) {
852                    LOG.debug("Error reading project OU.", e);
853                    projectName = projectName + " - " + project.getOuFqn();
854                }
855            }
856            result.put(project.getUuid(), projectName);
857        }
858        return result;
859
860    }
861
862    /**
863     * Gets the current Vaadin request, cast to a HttpServletRequest.<p>
864     *
865     * @return the current request
866     */
867    public static HttpServletRequest getRequest() {
868
869        return (HttpServletRequest)VaadinService.getCurrentRequest();
870    }
871
872    /**
873     * Gets list of resource types.<p>
874     *
875     * @return List
876     */
877    public static List<I_CmsResourceType> getResourceTypes() {
878
879        List<I_CmsResourceType> res = new ArrayList<I_CmsResourceType>();
880        for (I_CmsResourceType type : OpenCms.getResourceManager().getResourceTypes()) {
881            CmsExplorerTypeSettings typeSetting = OpenCms.getWorkplaceManager().getExplorerTypeSetting(
882                type.getTypeName());
883            if (typeSetting != null) {
884                res.add(type);
885            }
886        }
887        return res;
888    }
889
890    /**
891     * Returns the available resource types container.<p>
892     *
893     * @return the resource types container
894     */
895    public static IndexedContainer getResourceTypesContainer() {
896
897        IndexedContainer container = new IndexedContainer();
898        container.addContainerProperty(PropertyId.caption, String.class, null);
899        container.addContainerProperty(PropertyId.icon, Resource.class, null);
900        container.addContainerProperty(PropertyId.isFolder, Boolean.class, null);
901        container.addContainerProperty(PropertyId.isXmlContent, Boolean.class, null);
902        List<I_CmsResourceType> types = getResourceTypes();
903        sortResourceTypes(types);
904        for (I_CmsResourceType type : types) {
905            CmsExplorerTypeSettings typeSetting = OpenCms.getWorkplaceManager().getExplorerTypeSetting(
906                type.getTypeName());
907            Item typeItem = container.addItem(type);
908            String caption = CmsVaadinUtils.getMessageText(typeSetting.getKey()) + " (" + type.getTypeName() + ")";
909            typeItem.getItemProperty(PropertyId.caption).setValue(caption);
910            typeItem.getItemProperty(PropertyId.icon).setValue(CmsResourceUtil.getSmallIconResource(typeSetting, null));
911            typeItem.getItemProperty(PropertyId.isXmlContent).setValue(
912                Boolean.valueOf(type instanceof CmsResourceTypeXmlContent));
913            typeItem.getItemProperty(PropertyId.isFolder).setValue(
914                Boolean.valueOf(type instanceof A_CmsResourceTypeFolderBase));
915        }
916
917        return container;
918    }
919
920    /**
921     * Returns the roles available for a given user.<p>
922     *
923     * @param cms CmsObject
924     * @param user to get available roles for
925     * @param captionPropertyName name of caption property
926     * @return indexed container
927     */
928    public static IndexedContainer getRoleContainerForUser(CmsObject cms, CmsUser user, String captionPropertyName) {
929
930        IndexedContainer result = new IndexedContainer();
931        result.addContainerProperty(captionPropertyName, String.class, "");
932        try {
933            List<CmsRole> roles = OpenCms.getRoleManager().getRoles(cms, user.getOuFqn(), false);
934            CmsRole.applySystemRoleOrder(roles);
935            for (CmsRole role : roles) {
936                Item item = result.addItem(role);
937                item.getItemProperty(captionPropertyName).setValue(role.getDisplayName(cms, A_CmsUI.get().getLocale()));
938            }
939        } catch (CmsException e) {
940            LOG.error("Unabel to read roles for user", e);
941        }
942        return result;
943    }
944
945    /**
946     * Gets the window which contains a given component.<p>
947     *
948     * @param component the component
949     * @return the window containing the component, or null if no component is found
950     */
951    public static Window getWindow(Component component) {
952
953        if (component == null) {
954            return null;
955        } else if (component instanceof Window) {
956            return (Window)component;
957        } else {
958            return getWindow(component.getParent());
959        }
960
961    }
962
963    /**
964     * Get container with workpalce languages.<p>
965     *
966     * @param captionPropertyName name
967     * @return indexed container
968     */
969    public static IndexedContainer getWorkplaceLanguageContainer(String captionPropertyName) {
970
971        IndexedContainer result = new IndexedContainer();
972        result.addContainerProperty(captionPropertyName, String.class, "");
973        CmsLanguagePreference.getOptionMapForLanguage().forEach((locale, title) -> {
974            Item item = result.addItem(locale);
975            item.getItemProperty(captionPropertyName).setValue(title);
976
977        });
978
979        return result;
980    }
981
982    /**
983     * Gets the link to the (new) workplace.<p>
984     *
985     * @return the link to the workplace
986     */
987    public static String getWorkplaceLink() {
988
989        return OpenCms.getSystemInfo().getWorkplaceContext();
990    }
991
992    /**
993     * Returns the workplace link for the given app.<p>
994     *
995     * @param appId the app id
996     *
997     * @return the workplace link
998     */
999    public static String getWorkplaceLink(String appId) {
1000
1001        return getWorkplaceLink() + CmsAppWorkplaceUi.WORKPLACE_APP_ID_SEPARATOR + appId;
1002    }
1003
1004    /**
1005     * Returns the workplace link to the given app with the given state.<p>
1006     *
1007     * @param appId the app id
1008     * @param appState the app state
1009     *
1010     * @return the workplace link
1011     */
1012    public static String getWorkplaceLink(String appId, String appState) {
1013
1014        return getWorkplaceLink(appId) + CmsAppWorkplaceUi.WORKPLACE_STATE_SEPARATOR + appState;
1015    }
1016
1017    /**
1018     * Returns the workplace link to the given app with the given state including the given request parameters.<p>
1019     *
1020     * @param appId the app id
1021     * @param appState the app state
1022     * @param requestParameters the request parameters
1023     *
1024     * @return the workplace link
1025     */
1026    public static String getWorkplaceLink(String appId, String appState, Map<String, String[]> requestParameters) {
1027
1028        String result = getWorkplaceLink();
1029        if ((requestParameters != null) && !requestParameters.isEmpty()) {
1030            boolean first = true;
1031            for (Entry<String, String[]> param : requestParameters.entrySet()) {
1032                for (String value : param.getValue()) {
1033                    if (first) {
1034                        result += "?";
1035                    } else {
1036                        result += "&";
1037                    }
1038                    result += param.getKey() + "=" + value;
1039                    first = false;
1040                }
1041            }
1042        }
1043
1044        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(appId)) {
1045            result += CmsAppWorkplaceUi.WORKPLACE_APP_ID_SEPARATOR + appId;
1046        }
1047        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(appState)) {
1048            result += CmsAppWorkplaceUi.WORKPLACE_STATE_SEPARATOR + appState;
1049        }
1050        return result;
1051    }
1052
1053    /**
1054     * Gets external resource from workplace resource folder.<p>
1055     *
1056     * @param subPath path relative to workplace resource folder
1057     *
1058     * @return the external resource
1059     */
1060    public static ExternalResource getWorkplaceResource(String subPath) {
1061
1062        return new ExternalResource(CmsWorkplace.getResourceUri(subPath));
1063
1064    }
1065
1066    /**
1067     * Gets the workplace messages for the current locale.<p>
1068     *
1069     * @return the workplace messages
1070     */
1071    public static CmsMessages getWpMessagesForCurrentLocale() {
1072
1073        Locale locale;
1074        if (A_CmsUI.get() != null) {
1075            locale = A_CmsUI.get().getLocale();
1076        } else {
1077            if (LOG.isWarnEnabled()) {
1078                Exception e = new Exception("getWpMessagesForCurrentLocale called from non-Vaadin context");
1079                LOG.warn(e.getLocalizedMessage(), e);
1080            }
1081            locale = Locale.ENGLISH;
1082        }
1083        return OpenCms.getWorkplaceManager().getMessages(locale);
1084    }
1085
1086    /**
1087     * Checks if path is itemid in container.<p>
1088     *
1089     * @param cnt to be checked
1090     * @param path as itemid
1091     * @return true id path is itemid in container
1092     */
1093    public static boolean hasPathAsItemId(Container cnt, String path) {
1094
1095        return cnt.containsId(path) || cnt.containsId(CmsFileUtil.toggleTrailingSeparator(path));
1096    }
1097
1098    /**
1099     * Checks if a button is pressed.<p>
1100     *
1101     * @param button the button
1102     *
1103     * @return true if the button is pressed
1104     */
1105    public static boolean isButtonPressed(Button button) {
1106
1107        if (button == null) {
1108            return false;
1109        }
1110        List<String> styles = Arrays.asList(button.getStyleName().split(" "));
1111
1112        return styles.contains(OpenCmsTheme.BUTTON_PRESSED);
1113    }
1114
1115    /**
1116     * Uses the currently set locale to resolve localization macros in the input string using workplace message bundles.<p>
1117     *
1118     * @param baseString the string to localize
1119     *
1120     * @return the localized string
1121     */
1122    public static String localizeString(String baseString) {
1123
1124        if (baseString == null) {
1125            return null;
1126        }
1127        CmsWorkplaceMessages wpMessages = OpenCms.getWorkplaceManager().getMessages(A_CmsUI.get().getLocale());
1128        CmsMacroResolver resolver = new CmsMacroResolver();
1129        resolver.setMessages(wpMessages);
1130        String result = resolver.resolveMacros(baseString);
1131        return result;
1132    }
1133
1134    /**
1135     * Message accessior function.<p>
1136     *
1137     * @return the message for Cancel buttons
1138     */
1139    public static String messageCancel() {
1140
1141        return getMessageText(org.opencms.workplace.Messages.GUI_DIALOG_BUTTON_CANCEL_0);
1142    }
1143
1144    /**
1145     * Message accessior function.<p>
1146     *
1147     * @return the message for Cancel buttons
1148     */
1149    public static String messageClose() {
1150
1151        return getMessageText(org.opencms.workplace.Messages.GUI_DIALOG_BUTTON_CLOSE_0);
1152    }
1153
1154    /**
1155     * Message accessor function.<p>
1156     *
1157     * @return the message for OK buttons
1158     */
1159    public static String messageOk() {
1160
1161        return getMessageText(org.opencms.workplace.Messages.GUI_DIALOG_BUTTON_OK_0);
1162    }
1163
1164    /**
1165     * Generates the options items for the combo box using the map entry keys as values and the values as labels.<p>
1166     *
1167     * @param box the combo box to prepare
1168     * @param options the box options
1169     */
1170    public static void prepareComboBox(ComboBox box, Map<?, String> options) {
1171
1172        IndexedContainer container = new IndexedContainer();
1173        container.addContainerProperty(PROPERTY_VALUE, Object.class, null);
1174        container.addContainerProperty(PROPERTY_LABEL, String.class, "");
1175        for (Entry<?, String> entry : options.entrySet()) {
1176            Item item = container.addItem(entry.getKey());
1177            item.getItemProperty(PROPERTY_VALUE).setValue(entry.getKey());
1178            item.getItemProperty(PROPERTY_LABEL).setValue(entry.getValue());
1179        }
1180        box.setContainerDataSource(container);
1181        box.setItemCaptionPropertyId(PROPERTY_LABEL);
1182    }
1183
1184    /**
1185     * Reads the declarative design for a component and localizes it using a messages object.<p>
1186     *
1187     * The design will need to be located in the same directory as the component's class and have '.html' as a file extension.
1188     *
1189     * @param component the component for which to read the design
1190     * @param messages the message bundle to use for localization
1191     * @param macros the macros to use on the HTML template
1192     */
1193    @SuppressWarnings("resource")
1194    public static void readAndLocalizeDesign(Component component, CmsMessages messages, Map<String, String> macros) {
1195
1196        Class<?> componentClass = component.getClass();
1197        List<Class<?>> classes = Lists.newArrayList();
1198        classes.add(componentClass);
1199        classes.addAll(ClassUtils.getAllSuperclasses(componentClass));
1200        InputStream designStream = null;
1201        for (Class<?> cls : classes) {
1202            if (cls.getName().startsWith("com.vaadin")) {
1203                break;
1204            }
1205            String filename = cls.getSimpleName() + ".html";
1206            designStream = cls.getResourceAsStream(filename);
1207            if (designStream != null) {
1208                break;
1209            }
1210
1211        }
1212        if (designStream == null) {
1213            throw new IllegalArgumentException("Design not found for : " + component.getClass());
1214        }
1215        readAndLocalizeDesign(component, designStream, messages, macros);
1216    }
1217
1218    /**
1219     * Reads a layout from a resource, applies basic i18n macro substitution on the contained text, and returns a stream of the transformed
1220     * data.<p>
1221     *
1222     * @param layoutClass the class relative to which the layout resource will be looked up
1223     * @param relativeName the file name of the layout file
1224     *
1225     * @return an input stream which produces the transformed layout resource html
1226     */
1227    public static InputStream readCustomLayout(Class<? extends Component> layoutClass, String relativeName) {
1228
1229        CmsMacroResolver resolver = new CmsMacroResolver() {
1230
1231            @Override
1232            public String getMacroValue(String macro) {
1233
1234                return CmsEncoder.escapeXml(super.getMacroValue(macro));
1235            }
1236        };
1237        resolver.setMessages(CmsVaadinUtils.getWpMessagesForCurrentLocale());
1238        InputStream layoutStream = CmsVaadinUtils.filterUtf8ResourceStream(
1239            layoutClass.getResourceAsStream(relativeName),
1240            resolver.toFunction());
1241        return layoutStream;
1242    }
1243
1244    /**
1245     * Replaces component with new component.<p>
1246     *
1247     * @param component to be replaced
1248     * @param replacement new component
1249     */
1250    public static void replaceComponent(Component component, Component replacement) {
1251
1252        if (!component.isAttached()) {
1253            throw new IllegalArgumentException("Component must be attached");
1254        }
1255        HasComponents parent = component.getParent();
1256        if (parent instanceof ComponentContainer) {
1257            ((ComponentContainer)parent).replaceComponent(component, replacement);
1258        } else if (parent instanceof SingleComponentContainer) {
1259            ((SingleComponentContainer)parent).setContent(replacement);
1260        } else {
1261            throw new IllegalArgumentException("Illegal class for parent: " + parent.getClass());
1262        }
1263    }
1264
1265    /**
1266     * Configures a text field to look like a filter box for a table.
1267     *
1268     * @param searchBox the text field to configure
1269     */
1270    public static void setFilterBoxStyle(TextField searchBox) {
1271
1272        searchBox.setIcon(FontOpenCms.FILTER);
1273
1274        searchBox.setPlaceholder(
1275            org.opencms.ui.apps.Messages.get().getBundle(UI.getCurrent().getLocale()).key(
1276                org.opencms.ui.apps.Messages.GUI_EXPLORER_FILTER_0));
1277        searchBox.addStyleName(ValoTheme.TEXTFIELD_INLINE_ICON);
1278    }
1279
1280    /**
1281     * Sets the value of a text field which may be set to read-only mode.<p>
1282     *
1283     * When setting a Vaadin field to read-only, you also can't set its value programmatically anymore.
1284     * So we need to temporarily disable read-only mode, set the value, and then switch back to read-only mode.
1285     *
1286     * @param field the field
1287     * @param value the value to set
1288     */
1289    public static <T> void setReadonlyValue(AbstractField<T> field, T value) {
1290
1291        boolean readonly = field.isReadOnly();
1292        try {
1293            field.setReadOnly(false);
1294            field.setValue(value);
1295        } finally {
1296            field.setReadOnly(readonly);
1297        }
1298    }
1299
1300    /**
1301     * Shows an alert box to the user with the given information, which will perform the given action after the user clicks on OK.<p>
1302     *
1303     * @param title the title
1304     * @param message the message
1305     *
1306     * @param callback the callback to execute after clicking OK
1307     */
1308    public static void showAlert(String title, String message, final Runnable callback) {
1309
1310        final Window window = new Window();
1311        window.setModal(true);
1312        Panel panel = new Panel();
1313        panel.setCaption(title);
1314        panel.setWidth("500px");
1315        VerticalLayout layout = new VerticalLayout();
1316        layout.setMargin(true);
1317        panel.setContent(layout);
1318        layout.addComponent(new Label(message));
1319        Button okButton = new Button();
1320        okButton.addClickListener(new ClickListener() {
1321
1322            /** The serial version id. */
1323            private static final long serialVersionUID = 1L;
1324
1325            public void buttonClick(ClickEvent event) {
1326
1327                window.close();
1328                if (callback != null) {
1329                    callback.run();
1330                }
1331            }
1332        });
1333        layout.addComponent(okButton);
1334        layout.setComponentAlignment(okButton, Alignment.BOTTOM_RIGHT);
1335        okButton.setCaption(
1336            org.opencms.workplace.Messages.get().getBundle(A_CmsUI.get().getLocale()).key(
1337                org.opencms.workplace.Messages.GUI_DIALOG_BUTTON_OK_0));
1338        window.setContent(panel);
1339        window.setClosable(false);
1340        window.setResizable(false);
1341        A_CmsUI.get().addWindow(window);
1342
1343    }
1344
1345    /**
1346     * Sorts a list of resource types by their localized explorer type name.
1347     * @param resourceTypes the resource types
1348     */
1349    public static void sortResourceTypes(List<I_CmsResourceType> resourceTypes) {
1350
1351        Collections.sort(resourceTypes, (type, other) -> {
1352            CmsExplorerTypeSettings typeSetting = OpenCms.getWorkplaceManager().getExplorerTypeSetting(
1353                type.getTypeName());
1354            CmsExplorerTypeSettings otherSetting = OpenCms.getWorkplaceManager().getExplorerTypeSetting(
1355                other.getTypeName());
1356            if ((typeSetting != null) && (otherSetting != null)) {
1357                String typeName = CmsVaadinUtils.getMessageText(typeSetting.getKey());
1358                String otherName = CmsVaadinUtils.getMessageText(otherSetting.getKey());
1359                return typeName.compareTo(otherName);
1360            } else {
1361                return -1;
1362            }
1363        });
1364    }
1365
1366    /**
1367     * Creates a new option group builder.<p>
1368     *
1369     * @return a new option group builder
1370     */
1371    public static OptionGroupBuilder startOptionGroup() {
1372
1373        return new OptionGroupBuilder();
1374    }
1375
1376    /**
1377     * Sets style of a toggle button depending on its current state.<p>
1378     *
1379     * @param button the button to update
1380     */
1381    public static void toggleButton(Button button) {
1382
1383        if (isButtonPressed(button)) {
1384            button.removeStyleName(OpenCmsTheme.BUTTON_PRESSED);
1385        } else {
1386            button.addStyleName(OpenCmsTheme.BUTTON_PRESSED);
1387        }
1388    }
1389
1390    /**
1391     * Updates the component error of a component, but only if it differs from the currently set
1392     * error.<p>
1393     *
1394     * @param component the component
1395     * @param error the error
1396     *
1397     * @return true if the error was changed
1398     */
1399    public static boolean updateComponentError(AbstractComponent component, ErrorMessage error) {
1400
1401        if (component.getComponentError() != error) {
1402            component.setComponentError(error);
1403            return true;
1404        }
1405        return false;
1406    }
1407
1408    /**
1409     * Visits all descendants of a given component (including the component itself) and applies a predicate
1410     * to each.<p>
1411     *
1412     * If the predicate returns false for a component, no further descendants will be processed.<p>
1413     *
1414     * @param component the component
1415     * @param handler the predicate
1416     */
1417    public static void visitDescendants(Component component, Predicate<Component> handler) {
1418
1419        List<Component> stack = Lists.newArrayList();
1420        stack.add(component);
1421        while (!stack.isEmpty()) {
1422            Component currentComponent = stack.get(stack.size() - 1);
1423            stack.remove(stack.size() - 1);
1424            if (!handler.apply(currentComponent)) {
1425                return;
1426            }
1427            if (currentComponent instanceof HasComponents) {
1428                List<Component> children = Lists.newArrayList((HasComponents)currentComponent);
1429                Collections.reverse(children);
1430                stack.addAll(children);
1431            }
1432        }
1433    }
1434
1435    /**
1436     * Waggle the component.<p>
1437     *
1438     * @param component to be waggled
1439     */
1440    public static void waggleMeOnce(Component component) {
1441
1442        //TODO Until now, the component gets a waggler class which can not be removed again here..
1443        component.addStyleName("waggler");
1444        //Add JavaScript code, which adds the waggle class and removes it after a short time.
1445        JavaScript.getCurrent().execute(
1446            "waggler=document.querySelectorAll(\".waggler\")[0];"
1447                + "waggler.className=waggler.className + \" waggle\";"
1448                + "setTimeout(function () {\n"
1449                + "waggler.className=waggler.className.replace(/\\bwaggle\\b/g, \"\");"
1450                + "    }, 1500);");
1451    }
1452
1453    /**
1454     * Reads the given design and resolves the given macros and localizations.<p>
1455    
1456     * @param component the component whose design to read
1457     * @param designStream stream to read the design from
1458     * @param messages the message bundle to use for localization in the design (may be null)
1459     * @param macros other macros to substitute in the macro design (may be null)
1460     */
1461    protected static void readAndLocalizeDesign(
1462        Component component,
1463        InputStream designStream,
1464        CmsMessages messages,
1465        Map<String, String> macros) {
1466
1467        try {
1468            byte[] designBytes = CmsFileUtil.readFully(designStream, true);
1469            final String encoding = "UTF-8";
1470            String design = new String(designBytes, encoding);
1471
1472            CmsMacroResolver resolver = new CmsMacroResolver() {
1473
1474                @Override
1475                public String getMacroValue(String macro) {
1476
1477                    String result = super.getMacroValue(macro);
1478                    // The macro may contain quotes or angle brackets, so we need to escape the values for insertion into the design file
1479                    return CmsEncoder.escapeXml(result);
1480
1481                }
1482            };
1483
1484            if (macros != null) {
1485                for (Map.Entry<String, String> entry : macros.entrySet()) {
1486                    resolver.addMacro(entry.getKey(), entry.getValue());
1487                }
1488            }
1489            if (messages != null) {
1490                resolver.setMessages(messages);
1491            }
1492
1493            // workaround for existing HTML templates which use the incorrect <tag/> syntax, which sort of worked with previous versions of JSoup but not with the current one
1494            String correctedDesign = design.replaceAll("<(?!meta )([A-Za-z0-9-]+)( [^/>]*)/>", "<$1$2></$1>");
1495            if (!design.equals(correctedDesign)) {
1496                LOG.warn(
1497                    "Design was automatically corrected from \n"
1498                        + design
1499                        + "\n to \n"
1500                        + correctedDesign
1501                        + "\n\n--- Don't use XML-style empty-element syntax (<tag/>) in Vaadin HTML designs! ---\n");
1502                design = correctedDesign;
1503            }
1504            String resolvedDesign = resolver.resolveMacros(design);
1505            Design.read(new ByteArrayInputStream(resolvedDesign.getBytes(encoding)), component);
1506        } catch (IOException e) {
1507            throw new RuntimeException("Could not read design", e);
1508        } finally {
1509            try {
1510                designStream.close();
1511            } catch (IOException e) {
1512                LOG.warn(e.getLocalizedMessage(), e);
1513            }
1514        }
1515    }
1516
1517    /**
1518     * Returns whether only a single OU is visible to the current user.<p>
1519     *
1520     * @param projects the selectable projects
1521     *
1522     * @return <code>true</code> if only a single OU is visible to the current user
1523     */
1524    private static boolean isSingleOu(List<CmsProject> projects) {
1525
1526        String ouFqn = null;
1527        for (CmsProject project : projects) {
1528            if (project.isOnlineProject()) {
1529                // skip the online project
1530                continue;
1531            }
1532            if (ouFqn == null) {
1533                // set the first ou
1534                ouFqn = project.getOuFqn();
1535            } else if (!ouFqn.equals(project.getOuFqn())) {
1536                // break if one different ou is found
1537                return false;
1538            }
1539        }
1540        return true;
1541    }
1542
1543}