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 GmbH & Co. KG, 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.widgets;
029
030import org.opencms.acacia.shared.CmsWidgetUtil;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.CmsVfsResourceNotFoundException;
036import org.opencms.i18n.CmsMessages;
037import org.opencms.main.CmsException;
038import org.opencms.main.CmsLog;
039import org.opencms.main.OpenCms;
040import org.opencms.relations.CmsCategory;
041import org.opencms.relations.CmsCategoryService;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.util.CmsUUID;
044import org.opencms.workplace.CmsWorkplace;
045import org.opencms.xml.content.I_CmsXmlContentHandler.DisplayType;
046import org.opencms.xml.types.A_CmsXmlContentValue;
047import org.opencms.xml.types.CmsXmlCategoryValue;
048import org.opencms.xml.types.CmsXmlDynamicCategoryValue;
049import org.opencms.xml.types.I_CmsXmlContentValue;
050
051import java.util.ArrayList;
052import java.util.HashMap;
053import java.util.HashSet;
054import java.util.Iterator;
055import java.util.List;
056import java.util.Locale;
057import java.util.Map;
058import java.util.Set;
059
060import org.apache.commons.logging.Log;
061import org.apache.commons.text.StringEscapeUtils;
062
063/**
064 * Provides a widget for a category based dependent select boxes.<p>
065 *
066 * @since 7.0.3
067 */
068public class CmsCategoryWidget extends A_CmsWidget implements I_CmsADEWidget {
069
070    /** Configuration parameter to set the category to display. */
071    public static final String CONFIGURATION_CATEGORY = "category";
072
073    /** Configuration parameter to set the 'only leaf' flag parameter. */
074    public static final String CONFIGURATION_ONLYLEAFS = "onlyleafs";
075
076    /** Configuration parameter to set the 'property' parameter. */
077    public static final String CONFIGURATION_PROPERTY = "property";
078
079    /** Configuration parameter to set the 'selection type' parameter. */
080    private static final String CONFIGURATION_PARENTSELECTION = "parentselection";
081
082    /** Configuration parameter to set the 'selection type' parameter. */
083    private static final String CONFIGURATION_SELECTIONTYPE = "selectiontype";
084
085    /** Configuration parameter to set the collapsing state when opening the selection. */
086    private static final String CONFIGURATION_COLLAPSED = "collapsed";
087
088    /** Configuration parameter to set flag, indicating if categories should be shown separated by repository. */
089    private static final String CONFIGURATION_SHOW_WITH_REPOSITORY = "showwithrepository";
090
091    /** Configuration parameter to set flag, indicating if categories should be shown separated by repository. */
092    private static final String CONFIGURATION_REFPATH = "refpath";
093
094    /** The log object for this class. */
095    private static final Log LOG = CmsLog.getLog(CmsCategoryWidget.class);
096
097    /** The displayed category. */
098    private String m_category;
099
100    /** The 'only leaf' flag. */
101    private boolean m_onlyLeafs;
102
103    /** The value if the parents should be selected with the children. */
104    private boolean m_parentSelection;
105
106    /** The property to read the starting category from. */
107    private String m_property;
108
109    /** The selection type parsed from configuration string. */
110    private String m_selectiontype = "multi";
111
112    /**
113     * Creates a new category widget.<p>
114     */
115    public CmsCategoryWidget() {
116
117        // empty constructor is required for class registration
118        super();
119    }
120
121    /**
122     * Creates a category widget with the specified options.<p>
123     *
124     * @param configuration the configuration for the widget
125     */
126    public CmsCategoryWidget(String configuration) {
127
128        super(configuration);
129    }
130
131    /**
132     * @see org.opencms.widgets.A_CmsWidget#getConfiguration()
133     */
134    @Override
135    public String getConfiguration() {
136
137        Map<String, String> result = generateCommonConfigPart();
138
139        return CmsWidgetUtil.generatePipeSeparatedConfigString(result);
140    }
141
142    /**
143     * @see org.opencms.widgets.I_CmsADEWidget#getConfiguration(org.opencms.file.CmsObject, org.opencms.xml.types.A_CmsXmlContentValue, org.opencms.i18n.CmsMessages, org.opencms.file.CmsResource, java.util.Locale)
144     */
145    public String getConfiguration(
146        CmsObject cms,
147        A_CmsXmlContentValue schemaType,
148        CmsMessages messages,
149        CmsResource resource,
150        Locale contentLocale) {
151
152        // adjust 'selection type' according to schema type.
153        if (!m_selectiontype.equals("single")
154            && (schemaType.getTypeName().equals(CmsXmlCategoryValue.TYPE_NAME)
155                || schemaType.getTypeName().equals(CmsXmlDynamicCategoryValue.TYPE_NAME))) {
156            m_selectiontype = "multi";
157        } else {
158            m_selectiontype = "single";
159        }
160
161        // NOTE: set starting category as "category=" - independently if it is set via "property" or "category" config option.
162        m_category = this.getStartingCategory(cms, cms.getSitePath(resource));
163        Map<String, String> result = generateCommonConfigPart();
164        // append 'collapsed' flag, if necessary
165        if (OpenCms.getWorkplaceManager().isDisplayCategorySelectionCollapsed()) {
166            result.put(CONFIGURATION_COLLAPSED, null);
167        }
168        // append 'showWithCategory' flag, if necessary
169        if (OpenCms.getWorkplaceManager().isDisplayCategorySelectionCollapsed()) {
170            result.put(CONFIGURATION_SHOW_WITH_REPOSITORY, null);
171        }
172        if (m_parentSelection) {
173            result.put(CONFIGURATION_PARENTSELECTION, null);
174        }
175        result.put(CONFIGURATION_REFPATH, cms.getSitePath(resource));
176
177        return CmsWidgetUtil.generatePipeSeparatedConfigString(result);
178    }
179
180    /**
181     * @see org.opencms.widgets.I_CmsADEWidget#getCssResourceLinks(org.opencms.file.CmsObject)
182     */
183    public List<String> getCssResourceLinks(CmsObject cms) {
184
185        // nothing to do
186        return null;
187    }
188
189    /**
190     * @see org.opencms.widgets.I_CmsADEWidget#getDefaultDisplayType()
191     */
192    public DisplayType getDefaultDisplayType() {
193
194        return DisplayType.wide;
195    }
196
197    /**
198     * @see org.opencms.widgets.I_CmsWidget#getDialogIncludes(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog)
199     */
200    @Override
201    public String getDialogIncludes(CmsObject cms, I_CmsWidgetDialog widgetDialog) {
202
203        StringBuffer result = new StringBuffer(16);
204        result.append("<script  src=\"");
205        result.append(CmsWorkplace.getSkinUri());
206        result.append("components/widgets/category.js\"></script>\n");
207        return result.toString();
208    }
209
210    /**
211     * @see org.opencms.widgets.I_CmsWidget#getDialogWidget(org.opencms.file.CmsObject, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter)
212     */
213    public String getDialogWidget(CmsObject cms, I_CmsWidgetDialog widgetDialog, I_CmsWidgetParameter param) {
214
215        // get select box options from default value String
216        CmsCategory selected = null;
217        try {
218            String name = param.getStringValue(cms);
219            selected = CmsCategoryService.getInstance().getCategory(cms, name);
220        } catch (CmsException e) {
221            // ignore
222        }
223
224        StringBuffer result = new StringBuffer(16);
225        List<List<CmsSelectWidgetOption>> levels = new ArrayList<List<CmsSelectWidgetOption>>();
226        try {
227            // write arrays of categories
228            result.append("<script >\n");
229            String referencePath = null;
230            try {
231                referencePath = cms.getSitePath(getResource(cms, param));
232            } catch (Exception e) {
233                // ignore, this can happen if a new resource is edited using direct edit
234            }
235            String startingCat = getStartingCategory(cms, referencePath);
236            List<CmsCategory> cats = CmsCategoryService.getInstance().readCategories(
237                cms,
238                startingCat,
239                true,
240                referencePath);
241            cats = CmsCategoryService.getInstance().localizeCategories(
242                cms,
243                cats,
244                OpenCms.getWorkplaceManager().getWorkplaceLocale(cms));
245            int baseLevel;
246            if (CmsStringUtil.isEmptyOrWhitespaceOnly(startingCat)) {
247                baseLevel = 0;
248            } else {
249                baseLevel = CmsResource.getPathLevel(startingCat);
250                if (!(startingCat.startsWith("/") && startingCat.endsWith("/"))) {
251                    baseLevel++;
252                }
253            }
254            int level;
255            Set<String> done = new HashSet<String>();
256            List<CmsSelectWidgetOption> options = new ArrayList<CmsSelectWidgetOption>();
257            String jsId = CmsStringUtil.substitute(param.getId(), ".", "");
258            for (level = baseLevel + 1; !cats.isEmpty(); level++) {
259                if (level != (baseLevel + 1)) {
260                    result.append("var cat" + (level - baseLevel) + jsId + " = new Array(\n");
261                }
262                Iterator<CmsCategory> itSubs = cats.iterator();
263                while (itSubs.hasNext()) {
264                    CmsCategory cat = itSubs.next();
265                    String title = cat.getTitle();
266                    String titleJs = StringEscapeUtils.escapeEcmaScript(title);
267                    String titleHtml = StringEscapeUtils.escapeHtml4(title);
268                    if ((CmsResource.getPathLevel(cat.getPath()) + 1) == level) {
269                        itSubs.remove();
270                        if (done.contains(cat.getPath())) {
271                            continue;
272                        }
273                        if (level != (baseLevel + 1)) {
274                            result.append(
275                                "new Array('"
276                                    + cat.getId()
277                                    + "', '"
278                                    + CmsCategoryService.getInstance().readCategory(
279                                        cms,
280                                        CmsResource.getParentFolder(cat.getPath()),
281                                        referencePath).getId()
282                                    + "', '"
283                                    + titleJs
284                                    + "'),\n");
285                        }
286                        if ((level == (baseLevel + 1))
287                            || ((selected != null)
288                                && selected.getPath().startsWith(CmsResource.getParentFolder(cat.getPath())))) {
289                            if (levels.size() < (level - baseLevel)) {
290                                options = new ArrayList<CmsSelectWidgetOption>();
291                                levels.add(options);
292                                options.add(
293                                    new CmsSelectWidgetOption(
294                                        "",
295                                        true,
296                                        Messages.get().getBundle(widgetDialog.getLocale()).key(
297                                            Messages.GUI_CATEGORY_SELECT_0)));
298                            }
299                            options.add(new CmsSelectWidgetOption(cat.getId().toString(), false, titleHtml));
300                        }
301                        done.add(cat.getPath());
302                    }
303                }
304                if (level != (baseLevel + 1)) {
305                    result.deleteCharAt(result.length() - 1);
306                    result.deleteCharAt(result.length() - 1);
307                    result.append(");\n");
308                }
309            }
310            result.append("</script>\n");
311
312            result.append("<td class=\"xmlTd\" >");
313            result.append(
314                "<input id='"
315                    + param.getId()
316                    + "' name='"
317                    + param.getId()
318                    + "' type='hidden' value='"
319                    + (selected != null ? selected.getId().toString() : "")
320                    + "'>\n");
321
322            for (int i = 1; i < (level - baseLevel); i++) {
323                result.append("<span id='" + param.getId() + "cat" + i + "IdDisplay'");
324                if (levels.size() >= i) {
325                    options = levels.get(i - 1);
326                } else {
327                    result.append(" style='display:none'");
328                    options = new ArrayList<CmsSelectWidgetOption>();
329                    options.add(
330                        new CmsSelectWidgetOption(
331                            "",
332                            true,
333                            Messages.get().getBundle(widgetDialog.getLocale()).key(Messages.GUI_CATEGORY_SELECT_0)));
334                }
335                result.append(">");
336                result.append(
337                    buildSelectBox(
338                        param.getId(),
339                        i,
340                        options,
341                        (selected != null
342                        ? CmsCategoryService.getInstance().readCategory(
343                            cms,
344                            CmsResource.getPathPart(selected.getPath(), i + baseLevel),
345                            referencePath).getId().toString()
346                        : ""),
347                        param.hasError(),
348                        (i == (level - baseLevel - 1))));
349                result.append("</span>&nbsp;");
350            }
351            result.append("</td>");
352        } catch (CmsException e) {
353            result.append(e.getLocalizedMessage());
354        }
355        return result.toString();
356    }
357
358    /**
359     * @see org.opencms.widgets.I_CmsADEWidget#getInitCall()
360     */
361    public String getInitCall() {
362
363        // nothing to do
364        return null;
365    }
366
367    /**
368     * @see org.opencms.widgets.I_CmsADEWidget#getJavaScriptResourceLinks(org.opencms.file.CmsObject)
369     */
370    public List<String> getJavaScriptResourceLinks(CmsObject cms) {
371
372        // nothing to do
373        return null;
374    }
375
376    /**
377     * Returns the starting category depending on the configuration options.<p>
378     *
379     * @param cms the cms context
380     * @param referencePath the right resource path
381     *
382     * @return the starting category
383     */
384    public String getStartingCategory(CmsObject cms, String referencePath) {
385
386        String ret = "";
387        if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_category) && CmsStringUtil.isEmptyOrWhitespaceOnly(m_property)) {
388            ret = "/";
389        } else if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_property)) {
390            ret = m_category;
391        } else {
392            // use the given property from the right file
393            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(referencePath)) {
394                try {
395                    ret = cms.readPropertyObject(referencePath, m_property, true).getValue("/");
396                } catch (CmsException ex) {
397                    // should never happen
398                    if (LOG.isErrorEnabled()) {
399                        LOG.error(ex.getLocalizedMessage(), ex);
400                    }
401                }
402            }
403        }
404        if (!ret.endsWith("/")) {
405            ret += "/";
406        }
407        if (ret.startsWith("/")) {
408            ret = ret.substring(1);
409        }
410        return ret;
411    }
412
413    /**
414     * @see org.opencms.widgets.I_CmsADEWidget#getWidgetName()
415     */
416    public String getWidgetName() {
417
418        return CmsCategoryWidget.class.getName();
419    }
420
421    /**
422     * @see org.opencms.widgets.A_CmsWidget#isCompactViewEnabled()
423     */
424    @Override
425    public boolean isCompactViewEnabled() {
426
427        return false;
428    }
429
430    /**
431     * @see org.opencms.widgets.I_CmsADEWidget#isInternal()
432     */
433    public boolean isInternal() {
434
435        return false;
436    }
437
438    /**
439     * Check if only leaf selection is allowed.<p>
440     *
441     * @return <code>true</code>, if only leaf selection is allowed
442     */
443    public boolean isOnlyLeafs() {
444
445        return Boolean.valueOf(m_onlyLeafs).booleanValue();
446    }
447
448    /**
449     * @see org.opencms.widgets.I_CmsWidget#newInstance()
450     */
451    public I_CmsWidget newInstance() {
452
453        return new CmsCategoryWidget(getConfiguration());
454    }
455
456    /**
457     * @see org.opencms.widgets.A_CmsWidget#setConfiguration(java.lang.String)
458     */
459    @Override
460    public void setConfiguration(String configuration) {
461
462        Map<String, String> configOptions = CmsWidgetUtil.parsePipeSeparatedConfigString(configuration);
463        // we have to validate later, since we do not have any cms object here
464        m_category = "";
465        if (!configOptions.isEmpty()) {
466            m_category = CmsWidgetUtil.getStringOption(configOptions, CONFIGURATION_CATEGORY, m_category);
467            m_onlyLeafs = CmsWidgetUtil.getBooleanOption(configOptions, CONFIGURATION_ONLYLEAFS);
468            m_parentSelection = CmsWidgetUtil.getBooleanOption(configOptions, CONFIGURATION_PARENTSELECTION);
469            m_property = CmsWidgetUtil.getStringOption(configOptions, CONFIGURATION_PROPERTY, m_property);
470            m_selectiontype = CmsWidgetUtil.getStringOption(
471                configOptions,
472                CONFIGURATION_SELECTIONTYPE,
473                m_selectiontype);
474            if (null != m_selectiontype) {
475                m_selectiontype = m_selectiontype.toLowerCase();
476            }
477        }
478        super.setConfiguration(configuration);
479    }
480
481    /**
482     * @see org.opencms.widgets.A_CmsWidget#setEditorValue(org.opencms.file.CmsObject, java.util.Map, org.opencms.widgets.I_CmsWidgetDialog, org.opencms.widgets.I_CmsWidgetParameter)
483     */
484    @Override
485    public void setEditorValue(
486        CmsObject cms,
487        Map<String, String[]> formParameters,
488        I_CmsWidgetDialog widgetDialog,
489        I_CmsWidgetParameter param) {
490
491        super.setEditorValue(cms, formParameters, widgetDialog, param);
492        String id = param.getStringValue(cms);
493        if (CmsStringUtil.isEmptyOrWhitespaceOnly(id)) {
494            return;
495        }
496        try {
497            CmsCategory cat = CmsCategoryService.getInstance().getCategory(cms, cms.readResource(new CmsUUID(id)));
498            String referencePath = null;
499            try {
500                referencePath = cms.getSitePath(getResource(cms, param));
501            } catch (Exception e) {
502                // ignore, this can happen if a new resource is edited using direct edit
503            }
504            if (cat.getPath().startsWith(getStartingCategory(cms, referencePath))) {
505                param.setStringValue(cms, cat.getRootPath());
506            } else {
507                param.setStringValue(cms, "");
508            }
509        } catch (CmsException e) {
510            // invalid value
511            param.setStringValue(cms, "");
512        }
513    }
514
515    /**
516     * Generates html code for the category selection.<p>
517     *
518     * @param baseId the widget id
519     * @param level the category deep level
520     * @param options the list of {@link CmsSelectWidgetOption} objects
521     * @param selected the selected option
522     * @param hasError if to display error message
523     * @param last if it is the last level
524     *
525     * @return html code
526     */
527    protected String buildSelectBox(
528        String baseId,
529        int level,
530        List<CmsSelectWidgetOption> options,
531        String selected,
532        boolean hasError,
533        boolean last) {
534
535        StringBuffer result = new StringBuffer(16);
536        String id = baseId + "cat" + level + "Id";
537        String childId = baseId + "cat" + (level + 1) + "Id";
538        result.append("<select class=\"xmlInput");
539        if (hasError) {
540            result.append(" xmlInputError");
541        }
542        result.append("\" name=\"");
543        result.append(id);
544        result.append("\" id=\"");
545        result.append(id);
546        result.append("\" onchange=\"");
547        if (last) {
548            result.append("setWidgetValue('" + baseId + "');");
549        } else {
550            String jsId = CmsStringUtil.substitute(baseId, ".", "");
551            result.append("setChildListBox(this, getElemById('" + childId + "'), cat" + (level + 1) + jsId + ");");
552        }
553        result.append("\">");
554
555        Iterator<CmsSelectWidgetOption> i = options.iterator();
556        while (i.hasNext()) {
557            CmsSelectWidgetOption option = i.next();
558            // create the option
559            result.append("<option value=\"");
560            result.append(option.getValue());
561            result.append("\"");
562            if ((selected != null) && selected.equals(option.getValue())) {
563                result.append(" selected=\"selected\"");
564            }
565            result.append(">");
566            result.append(option.getOption());
567            result.append("</option>");
568        }
569        result.append("</select>");
570        return result.toString();
571    }
572
573    /**
574     * Returns the default locale in the content of the given resource.<p>
575     *
576     * @param cms the cms context
577     * @param resource the resource path to get the default locale for
578     *
579     * @return the default locale of the resource
580     */
581    protected Locale getDefaultLocale(CmsObject cms, String resource) {
582
583        Locale locale = OpenCms.getLocaleManager().getDefaultLocale(cms, resource);
584        if (locale == null) {
585            List<Locale> locales = OpenCms.getLocaleManager().getAvailableLocales();
586            if (locales.size() > 0) {
587                locale = locales.get(0);
588            } else {
589                locale = Locale.ENGLISH;
590            }
591        }
592        return locale;
593    }
594
595    /**
596     * Returns the right resource, depending on the locale.<p>
597     *
598     * @param cms the cms context
599     * @param param the widget parameter
600     *
601     * @return the resource to get/set the categories for
602     */
603    protected CmsResource getResource(CmsObject cms, I_CmsWidgetParameter param) {
604
605        I_CmsXmlContentValue value = (I_CmsXmlContentValue)param;
606        CmsFile file = value.getDocument().getFile();
607        String resourceName = cms.getSitePath(file);
608        if (CmsWorkplace.isTemporaryFile(file)) {
609            StringBuffer result = new StringBuffer(resourceName.length() + 2);
610            result.append(CmsResource.getFolderPath(resourceName));
611            result.append(CmsResource.getName(resourceName).substring(1));
612            resourceName = result.toString();
613        }
614        try {
615            List<CmsResource> listsib = cms.readSiblings(resourceName, CmsResourceFilter.ALL);
616            for (int i = 0; i < listsib.size(); i++) {
617                CmsResource resource = listsib.get(i);
618                // get the default locale of the resource
619                Locale locale = getDefaultLocale(cms, cms.getSitePath(resource));
620                if (locale.equals(value.getLocale())) {
621                    // get the property for the right locale
622                    return resource;
623                }
624            }
625        } catch (CmsVfsResourceNotFoundException e) {
626            // may hapen if editing a new resource
627            if (LOG.isDebugEnabled()) {
628                LOG.debug(e.getLocalizedMessage(), e);
629            }
630        } catch (CmsException e) {
631            if (LOG.isErrorEnabled()) {
632                LOG.error(e.getLocalizedMessage(), e);
633            }
634        }
635        return file;
636    }
637
638    /**
639     * Helper to generate the common configuration part for client-side and server-side widget.
640     * @return the common configuration options as map
641     */
642    private Map<String, String> generateCommonConfigPart() {
643
644        Map<String, String> result = new HashMap<>();
645        if (m_category != null) {
646            result.put(CONFIGURATION_CATEGORY, m_category);
647        }
648        // append 'only leafs' to configuration
649        if (m_onlyLeafs) {
650            result.put(CONFIGURATION_ONLYLEAFS, null);
651        }
652        // append 'property' to configuration
653        if (m_property != null) {
654            result.put(CONFIGURATION_PROPERTY, m_property);
655        }
656        // append 'selectionType' to configuration
657        if (m_selectiontype != null) {
658            result.put(CONFIGURATION_SELECTIONTYPE, m_selectiontype);
659        }
660        return result;
661    }
662}