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     * Removes all parameter from given state.<p>
232     *
233     * @param state state to be cleaned from parameter
234     * @return given state without parameter
235     */
236    public static String removeParamsFromState(String state) {
237
238        return state.split(PARAM_SEPARATOR)[0];
239    }
240
241    /**
242     * Gets an offline version of the cms object.<p>
243     *
244     * @param cms initial CmsObject
245     * @return CmsObject adjusted to offline project (cloned)
246     */
247    public CmsObject getOfflineCmsObject(CmsObject cms) {
248
249        CmsObject res = null;
250        try {
251            if (!cms.getRequestContext().getCurrentProject().isOnlineProject()) {
252                return OpenCms.initCmsObject(cms);
253            }
254            res = OpenCms.initCmsObject(cms);
255            List<CmsProject> projects = OpenCms.getOrgUnitManager().getAllAccessibleProjects(res, "/", true);
256            Iterator<CmsProject> projIterator = projects.iterator();
257            boolean offFound = false;
258            while (projIterator.hasNext() & !offFound) {
259                CmsProject offP = projIterator.next();
260                if (!offP.isOnlineProject()) {
261                    res.getRequestContext().setCurrentProject(offP);
262                    offFound = true;
263                }
264            }
265        } catch (CmsException e) {
266            return cms;
267        }
268        return res;
269    }
270
271    /**
272     * @see org.opencms.ui.apps.I_CmsWorkplaceApp#initUI(org.opencms.ui.apps.I_CmsAppUIContext)
273     */
274    public void initUI(I_CmsAppUIContext context) {
275
276        m_uiContext = context;
277        m_uiContext.showInfoArea(true);
278        m_breadCrumb = new CmsBreadCrumb();
279        m_infoLayout = new HorizontalLayout();
280        m_infoLayout.setSizeFull();
281        m_infoLayout.setSpacing(true);
282        m_infoLayout.setMargin(true);
283        m_uiContext.setAppInfo(m_infoLayout);
284        m_infoLayout.addComponent(m_breadCrumb);
285        m_infoLayout.setExpandRatio(m_breadCrumb, 2);
286        m_uiContext.setAppContent(m_rootLayout);
287    }
288
289    /**
290     * @see org.opencms.ui.apps.I_CmsWorkplaceApp#onStateChange(java.lang.String)
291     */
292    public void onStateChange(String state) {
293
294        openSubView(state, false);
295    }
296
297    /**
298     * Opens the requested sub view.<p>
299     *
300     * @param state the state
301     * @param updateState <code>true</code> to update the state URL token
302     */
303    public void openSubView(String state, boolean updateState) {
304
305        if (updateState) {
306            CmsAppWorkplaceUi.get().changeCurrentAppState(state);
307        }
308        Component comp = getComponentForState(state);
309        if (comp != null) {
310            comp.setSizeFull();
311            m_rootLayout.setMainContent(comp);
312        } else {
313            m_rootLayout.setMainContent(new Label("Malformed path, tool not available for path: " + state));
314        }
315        updateSubNav(getSubNavEntries(state));
316        updateBreadCrumb(getBreadCrumbForState(state));
317    }
318
319    /**
320     * Adds a navigation entry.<p>
321     *
322     * @param navEntry the navigation entry
323     */
324    protected void addSubNavEntry(final NavEntry navEntry) {
325
326        Button button = m_rootLayout.addSubNavEntry(navEntry);
327        button.addClickListener(new ClickListener() {
328
329            private static final long serialVersionUID = 1L;
330
331            public void buttonClick(ClickEvent event) {
332
333                openSubView(navEntry.getTargetState(), true);
334            }
335        });
336    }
337
338    /**
339     * Returns the current bread crumb entries in an ordered map.<p>
340     *
341     * @param state the current state
342     *
343     * @return bread crumb entry name by state, in case the state is empty, the entry will be disabled
344     */
345    protected abstract LinkedHashMap<String, String> getBreadCrumbForState(String state);
346
347    /**
348     * Returns the app component for the given state.<p>
349     *
350     * @param state the state to render
351     *
352     * @return the app component
353     */
354    protected abstract Component getComponentForState(String state);
355
356    /**
357     * Returns the last path level.<p>
358     *
359     * @param path the path
360     *
361     * @return the last path level
362     */
363    protected String getLastPathLevel(String path) {
364
365        path = path.trim();
366        if (path.endsWith("/")) {
367            path = path.substring(0, path.length() - 1);
368        }
369        if (path.contains("/")) {
370            path = path.substring(path.lastIndexOf("/"));
371        }
372        return path;
373    }
374
375    /**
376     * Returns the sub navigation entries.<p>
377     *
378     * @param state the state
379     *
380     * @return the sub navigation entries
381     */
382    protected abstract List<NavEntry> getSubNavEntries(String state);
383
384    /**
385     * Method to set bread crumb entries.<p>
386     *
387     * @param entries to be set
388     */
389    protected void setBreadCrumbEntries(LinkedHashMap<String, String> entries) {
390
391        m_breadCrumb.setEntries(entries);
392    }
393
394    /**
395     * Updates the bread crumb navigation.<p>
396     *
397     * @param breadCrumbEntries the bread crumb entries
398     */
399    protected void updateBreadCrumb(Map<String, String> breadCrumbEntries) {
400
401        LinkedHashMap<String, String> entries = new LinkedHashMap<String, String>();
402        I_CmsWorkplaceAppConfiguration launchpadConfig = OpenCms.getWorkplaceAppManager().getAppConfiguration(
403            CmsAppHierarchyConfiguration.APP_ID);
404        if (launchpadConfig.getVisibility(A_CmsUI.getCmsObject()).isActive()) {
405            entries.put(CmsAppHierarchyConfiguration.APP_ID, launchpadConfig.getName(UI.getCurrent().getLocale()));
406        }
407        if ((breadCrumbEntries != null) && !breadCrumbEntries.isEmpty()) {
408            entries.putAll(breadCrumbEntries);
409        } else {
410            entries.put(
411                "",
412                OpenCms.getWorkplaceAppManager().getAppConfiguration(m_uiContext.getAppId()).getName(
413                    UI.getCurrent().getLocale()));
414        }
415        m_breadCrumb.setEntries(entries);
416    }
417
418    /**
419     * Updates the sub navigation with the given entries.<p>
420     *
421     * @param subEntries the sub navigation entries
422     */
423    protected void updateSubNav(List<NavEntry> subEntries) {
424
425        m_rootLayout.clearSubNav();
426        if ((subEntries == null) || subEntries.isEmpty()) {
427            m_rootLayout.setSubNavVisible(false);
428        } else {
429            m_rootLayout.setSubNavVisible(true);
430            for (NavEntry entry : subEntries) {
431                addSubNavEntry(entry);
432            }
433        }
434    }
435}