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.apps;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProject;
032import org.opencms.i18n.CmsEncoder;
033import org.opencms.main.CmsException;
034import org.opencms.main.OpenCms;
035import org.opencms.ui.A_CmsUI;
036import org.opencms.ui.components.CmsBreadCrumb;
037import org.opencms.ui.components.CmsToolLayout;
038import org.opencms.util.CmsStringUtil;
039
040import java.util.HashMap;
041import java.util.Iterator;
042import java.util.LinkedHashMap;
043import java.util.List;
044import java.util.Map;
045
046import com.vaadin.server.Resource;
047import com.vaadin.ui.Button;
048import com.vaadin.ui.Button.ClickEvent;
049import com.vaadin.ui.Button.ClickListener;
050import com.vaadin.ui.Component;
051import com.vaadin.ui.HorizontalLayout;
052import com.vaadin.ui.Label;
053import com.vaadin.ui.UI;
054
055/**
056 * Super class for workplace apps to help implementing the app navigation and layout.<p>
057 */
058public abstract class A_CmsWorkplaceApp implements I_CmsWorkplaceApp {
059
060    /**
061     * An app navigation entry.<p>
062     */
063    public static class NavEntry {
064
065        /** The entry description. */
066        private String m_description;
067
068        /** The entry icon. */
069        private Resource m_icon;
070
071        /** The localized entry name. */
072        private String m_name;
073
074        /** The target state. */
075        private String m_targetState;
076
077        /**
078         * Constructor.<p>
079         *
080         * @param name the entry name
081         * @param description the description
082         * @param icon the icon
083         * @param targetState the target state
084         */
085        public NavEntry(String name, String description, Resource icon, String targetState) {
086
087            m_name = name;
088            m_description = description;
089            m_icon = icon;
090            m_targetState = targetState;
091        }
092
093        /**
094         * Returns the description.<p>
095         *
096         * @return the description
097         */
098        public String getDescription() {
099
100            return m_description;
101        }
102
103        /**
104         * Returns the icon.<p>
105         *
106         * @return the icon
107         */
108        public Resource getIcon() {
109
110            return m_icon;
111        }
112
113        /**
114         * Returns the entry name.<p>
115         *
116         * @return the entry name
117         */
118        public String getName() {
119
120            return m_name;
121        }
122
123        /**
124         * Returns the target state.<p>
125         *
126         * @return the target state
127         */
128        public String getTargetState() {
129
130            return m_targetState;
131        }
132    }
133
134    /** State parameter value separator. */
135    public static final String PARAM_ASSIGN = "::";
136
137    /** State parameter separator. */
138    public static final String PARAM_SEPARATOR = "!!";
139
140    /** The app info layout containing the bread crumb navigation as first component. */
141    protected HorizontalLayout m_infoLayout;
142
143    /** The root layout. */
144    protected CmsToolLayout m_rootLayout;
145
146    /** The app UI context. */
147    protected I_CmsAppUIContext m_uiContext;
148
149    /** The bread crumb navigation. */
150    private CmsBreadCrumb m_breadCrumb;
151
152    /**
153     * Constructor.<p>
154     */
155    protected A_CmsWorkplaceApp() {
156
157        m_rootLayout = new CmsToolLayout();
158        m_rootLayout.setSizeFull();
159    }
160
161    /**
162     * Adds a parameter value to the given state.<p>
163     *
164     * @param state the state
165     * @param paramName the parameter name
166     * @param value the parameter value
167     *
168     * @return the state
169     */
170    public static String addParamToState(String state, String paramName, String value) {
171
172        return state + PARAM_SEPARATOR + paramName + PARAM_ASSIGN + CmsEncoder.encode(value, CmsEncoder.ENCODING_UTF_8);
173    }
174
175    /**
176     * Parses the requested parameter from the given state.<p>
177     *
178     * @param state the state
179     * @param paramName the parameter name
180     *
181     * @return the parameter value
182     */
183    public static String getParamFromState(String state, String paramName) {
184
185        String prefix = PARAM_SEPARATOR + paramName + PARAM_ASSIGN;
186        if (state.contains(prefix)) {
187            String result = state.substring(state.indexOf(prefix) + prefix.length());
188            if (result.contains(PARAM_SEPARATOR)) {
189                result = result.substring(0, result.indexOf(PARAM_SEPARATOR));
190            }
191            return CmsEncoder.decode(result, CmsEncoder.ENCODING_UTF_8);
192        }
193        return null;
194    }
195
196    /**
197     * Returns the parameters contained in the state string.<p>
198     *
199     * @param state the state
200     *
201     * @return the parameters
202     */
203    public static Map<String, String> getParamsFromState(String state) {
204
205        Map<String, String> result = new HashMap<String, String>();
206        int separatorIndex = state.indexOf(PARAM_SEPARATOR);
207        while (separatorIndex >= 0) {
208            state = state.substring(separatorIndex + 2);
209            int assignIndex = state.indexOf(PARAM_ASSIGN);
210            if (assignIndex > 0) {
211                String key = state.substring(0, assignIndex);
212                state = state.substring(assignIndex + 2);
213                separatorIndex = state.indexOf(PARAM_SEPARATOR);
214                String value = null;
215                if (separatorIndex < 0) {
216                    value = state;
217                } else if (separatorIndex > 0) {
218                    value = state.substring(0, separatorIndex);
219                }
220                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) {
221                    result.put(key, CmsEncoder.decode(value, CmsEncoder.ENCODING_UTF_8));
222                }
223            } else {
224                separatorIndex = -1;
225            }
226        }
227        return result;
228    }
229
230    /**
231     * Gets an offline version of the cms object.<p>
232     *
233     * @param cms initial CmsObject
234     * @return CmsObject adjusted to offline project (cloned)
235     */
236    public CmsObject getOfflineCmsObject(CmsObject cms) {
237
238        CmsObject res = null;
239        try {
240            if (!cms.getRequestContext().getCurrentProject().isOnlineProject()) {
241                return OpenCms.initCmsObject(cms);
242            }
243            res = OpenCms.initCmsObject(cms);
244            List<CmsProject> projects = OpenCms.getOrgUnitManager().getAllAccessibleProjects(res, "/", true);
245            Iterator<CmsProject> projIterator = projects.iterator();
246            boolean offFound = false;
247            while (projIterator.hasNext() & !offFound) {
248                CmsProject offP = projIterator.next();
249                if (!offP.isOnlineProject()) {
250                    res.getRequestContext().setCurrentProject(offP);
251                    offFound = true;
252                }
253            }
254        } catch (CmsException e) {
255            return cms;
256        }
257        return res;
258    }
259
260    /**
261     * @see org.opencms.ui.apps.I_CmsWorkplaceApp#initUI(org.opencms.ui.apps.I_CmsAppUIContext)
262     */
263    public void initUI(I_CmsAppUIContext context) {
264
265        m_uiContext = context;
266        m_uiContext.showInfoArea(true);
267        m_breadCrumb = new CmsBreadCrumb();
268        m_infoLayout = new HorizontalLayout();
269        m_infoLayout.setSizeFull();
270        m_infoLayout.setSpacing(true);
271        m_infoLayout.setMargin(true);
272        m_uiContext.setAppInfo(m_infoLayout);
273        m_infoLayout.addComponent(m_breadCrumb);
274        m_infoLayout.setExpandRatio(m_breadCrumb, 2);
275        m_uiContext.setAppContent(m_rootLayout);
276    }
277
278    /**
279     * @see org.opencms.ui.apps.I_CmsWorkplaceApp#onStateChange(java.lang.String)
280     */
281    public void onStateChange(String state) {
282
283        openSubView(state, false);
284    }
285
286    /**
287     * Opens the requested sub view.<p>
288     *
289     * @param state the state
290     * @param updateState <code>true</code> to update the state URL token
291     */
292    public void openSubView(String state, boolean updateState) {
293
294        if (updateState) {
295            CmsAppWorkplaceUi.get().changeCurrentAppState(state);
296        }
297        Component comp = getComponentForState(state);
298        if (comp != null) {
299            comp.setSizeFull();
300            m_rootLayout.setMainContent(comp);
301        } else {
302            m_rootLayout.setMainContent(new Label("Malformed path, tool not available for path: " + state));
303        }
304        updateSubNav(getSubNavEntries(state));
305        updateBreadCrumb(getBreadCrumbForState(state));
306    }
307
308    /**
309     * Adds a navigation entry.<p>
310     *
311     * @param navEntry the navigation entry
312     */
313    protected void addSubNavEntry(final NavEntry navEntry) {
314
315        Button button = m_rootLayout.addSubNavEntry(navEntry);
316        button.addClickListener(new ClickListener() {
317
318            private static final long serialVersionUID = 1L;
319
320            public void buttonClick(ClickEvent event) {
321
322                openSubView(navEntry.getTargetState(), true);
323            }
324        });
325    }
326
327    /**
328     * Returns the current bread crumb entries in an ordered map.<p>
329     *
330     * @param state the current state
331     *
332     * @return bread crumb entry name by state, in case the state is empty, the entry will be disabled
333     */
334    protected abstract LinkedHashMap<String, String> getBreadCrumbForState(String state);
335
336    /**
337     * Returns the app component for the given state.<p>
338     *
339     * @param state the state to render
340     *
341     * @return the app component
342     */
343    protected abstract Component getComponentForState(String state);
344
345    /**
346     * Returns the last path level.<p>
347     *
348     * @param path the path
349     *
350     * @return the last path level
351     */
352    protected String getLastPathLevel(String path) {
353
354        path = path.trim();
355        if (path.endsWith("/")) {
356            path = path.substring(0, path.length() - 1);
357        }
358        if (path.contains("/")) {
359            path = path.substring(path.lastIndexOf("/"));
360        }
361        return path;
362    }
363
364    /**
365     * Returns the sub navigation entries.<p>
366     *
367     * @param state the state
368     *
369     * @return the sub navigation entries
370     */
371    protected abstract List<NavEntry> getSubNavEntries(String state);
372
373    /**
374     * Method to set bread crumb entries.<p>
375     *
376     * @param entries to be set
377     */
378    protected void setBreadCrumbEntries(LinkedHashMap<String, String> entries) {
379
380        m_breadCrumb.setEntries(entries);
381    }
382
383    /**
384     * Updates the bread crumb navigation.<p>
385     *
386     * @param breadCrumbEntries the bread crumb entries
387     */
388    protected void updateBreadCrumb(Map<String, String> breadCrumbEntries) {
389
390        LinkedHashMap<String, String> entries = new LinkedHashMap<String, String>();
391        I_CmsWorkplaceAppConfiguration launchpadConfig = OpenCms.getWorkplaceAppManager().getAppConfiguration(
392            CmsAppHierarchyConfiguration.APP_ID);
393        if (launchpadConfig.getVisibility(A_CmsUI.getCmsObject()).isActive()) {
394            entries.put(CmsAppHierarchyConfiguration.APP_ID, launchpadConfig.getName(UI.getCurrent().getLocale()));
395        }
396        if ((breadCrumbEntries != null) && !breadCrumbEntries.isEmpty()) {
397            entries.putAll(breadCrumbEntries);
398        } else {
399            entries.put(
400                "",
401                OpenCms.getWorkplaceAppManager().getAppConfiguration(m_uiContext.getAppId()).getName(
402                    UI.getCurrent().getLocale()));
403        }
404        m_breadCrumb.setEntries(entries);
405    }
406
407    /**
408     * Updates the sub navigation with the given entries.<p>
409     *
410     * @param subEntries the sub navigation entries
411     */
412    protected void updateSubNav(List<NavEntry> subEntries) {
413
414        m_rootLayout.clearSubNav();
415        if ((subEntries == null) || subEntries.isEmpty()) {
416            m_rootLayout.setSubNavVisible(false);
417        } else {
418            m_rootLayout.setSubNavVisible(true);
419            for (NavEntry entry : subEntries) {
420                addSubNavEntry(entry);
421            }
422        }
423    }
424}