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