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.xml.xml2json.renderer;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.configuration.CmsFormatterUtils;
032import org.opencms.ade.containerpage.shared.CmsFormatterConfig;
033import org.opencms.file.CmsFile;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsResource;
036import org.opencms.json.JSONArray;
037import org.opencms.json.JSONException;
038import org.opencms.json.JSONObject;
039import org.opencms.main.CmsException;
040import org.opencms.main.CmsLog;
041import org.opencms.main.OpenCms;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.util.CmsUUID;
044import org.opencms.xml.containerpage.CmsContainerBean;
045import org.opencms.xml.containerpage.CmsContainerElementBean;
046import org.opencms.xml.containerpage.CmsContainerPageBean;
047import org.opencms.xml.containerpage.CmsXmlContainerPage;
048import org.opencms.xml.containerpage.CmsXmlContainerPageFactory;
049import org.opencms.xml.containerpage.I_CmsFormatterBean;
050
051import java.util.ArrayList;
052import java.util.Collection;
053import java.util.Collections;
054import java.util.HashMap;
055import java.util.List;
056import java.util.Locale;
057import java.util.Map;
058import java.util.function.Predicate;
059
060import org.apache.commons.logging.Log;
061
062/**
063 * Used for rendering container pages as a JSON structure.
064 */
065public class CmsJsonRendererContainerPage {
066
067    /**
068     * Tree node wrapper for a container.
069     */
070    public class ContainerNode {
071
072        /** The container bean. */
073        private CmsContainerBean m_container;
074
075        /** List of nodes corresponding to container elements. */
076        private List<ElementNode> m_elements = new ArrayList<>();
077
078        /**
079         * Creates a new node for the given container.
080         *
081         * @param container the container bean
082         */
083        public ContainerNode(CmsContainerBean container) {
084
085            m_container = container;
086        }
087
088        /**
089         * Adds a container element subnode.
090         *
091         * @param elemNode the container element node
092         */
093        public void add(ElementNode elemNode) {
094
095            m_elements.add(elemNode);
096        }
097
098        /**
099         * Gets the container bean.
100         *
101         * @return the container bean
102         */
103        public CmsContainerBean getContainer() {
104
105            return m_container;
106        }
107
108        /**
109         * Gets the nodes corresponding to the container elements.
110         *
111         * @return the nodes for the container elements
112         */
113        public List<ElementNode> getElements() {
114
115            return Collections.unmodifiableList(m_elements);
116        }
117
118        /**
119         * Gets the container name.
120         *
121         * @return the container name
122         */
123        public String getName() {
124
125            return m_container.getName();
126        }
127
128        /**
129         * Returns the container type.
130         *
131         * @return the container type
132         */
133        public String getType() {
134
135            return m_container.getType();
136        }
137
138        /**
139         * Returns whether this container is a detail only container.
140         *
141         * @return whether a detail only container or not
142         */
143        public boolean isDetailOnlyContainer() {
144
145            return m_container.isDetailOnly();
146        }
147
148        /**
149         * Returns whether this container is a nested container.
150         *
151         * @return whether a nested container or not
152         */
153        public boolean isNestedContainer() {
154
155            return m_container.isNestedContainer();
156        }
157
158        /**
159         * Returns whether this container is a root container.
160         *
161         * @return whether a root container or not
162         */
163        public boolean isRootContainer() {
164
165            return m_container.isRootContainer();
166        }
167    }
168
169    /**
170     * Tree node wrapper around a container element.
171     */
172    public class ElementNode {
173
174        /** The map of sub-containers by name. */
175        private Map<String, ContainerNode> m_containers = new HashMap<>();
176
177        /** The element. */
178        private CmsContainerElementBean m_element;
179
180        /** The parent container. */
181        private ContainerNode m_parentContainerNode;
182
183        /**
184         * Creates a new element node.
185         *
186         * @param elementBean the container element bean
187         * @param parentContainerNode the parent container node
188         */
189        public ElementNode(CmsContainerElementBean elementBean, ContainerNode parentContainerNode) {
190
191            m_element = elementBean;
192            m_parentContainerNode = parentContainerNode;
193        }
194
195        /**
196         * Adds the container node as a nested container for the element.
197         *
198         * @param containerNode the container node
199         */
200        public void add(ContainerNode containerNode) {
201
202            m_containers.put(containerNode.getName(), containerNode);
203        }
204
205        /**
206         * Gets the nested containers for this element as a map, with container names as keys.
207         *
208         * @return the map of nested containers
209         */
210        public Map<String, ContainerNode> getContainers() {
211
212            return Collections.unmodifiableMap(m_containers);
213        }
214
215        /**
216         * Gets the container element bean which this node is wrapping.
217         *
218         * @return the container element bean
219         */
220        public CmsContainerElementBean getElement() {
221
222            return m_element;
223        }
224
225        /**
226         * Returns the parent container node.
227         *
228         * @return the parent container node
229         */
230        public ContainerNode getParentContainerNode() {
231
232            return m_parentContainerNode;
233        }
234
235    }
236
237    /** Logger instance for this class. */
238    private static final Log LOG = CmsLog.getLog(CmsJsonRendererContainerPage.class);
239
240    /** The CMS context used for VFS operations. */
241    private CmsObject m_cms;
242
243    /** The container page. */
244    private CmsResource m_page;
245
246    /** The property filter. */
247    private Predicate<String> m_propFilter;
248
249    /**
250     * Creates a new renderer instance.
251     *
252     * @param cms the CMS context
253     * @param page the container page to render
254     * @param propertyFilter the property filter
255     */
256    public CmsJsonRendererContainerPage(CmsObject cms, CmsResource page, Predicate<String> propertyFilter) {
257
258        m_cms = cms;
259        m_page = page;
260        m_propFilter = propertyFilter;
261    }
262
263    /**
264     * Builds a tree from the given container page bean.<p>
265     *
266     * The returned tree consists of container nodes (which have children corresponding the container elements) and element nodes
267     * (which have children corresponding to the nested containers of the element). The root of the tree is a dummy element node
268     * which does not correspond to any element in the page, but just acts as a container for the top-level containers of the page.
269     *
270     * @param page the container page bean
271     * @param rootPath the root path of the container page
272     * @return the dummy root element node
273     */
274    public ElementNode buildTree(CmsContainerPageBean page, String rootPath) {
275
276        Map<String, ElementNode> elementsByInstanceId = new HashMap<>();
277        List<ContainerNode> containerNodes = new ArrayList<>();
278        CmsADEConfigData adeConfig = OpenCms.getADEManager().lookupConfiguration(m_cms, rootPath);
279
280        for (CmsContainerBean container : page.getContainers().values()) {
281            ContainerNode containerNode = new ContainerNode(container);
282            containerNodes.add(containerNode);
283            for (CmsContainerElementBean cachedElementBean : container.getElements()) {
284                CmsContainerElementBean elementBean = cachedElementBean.clone();
285                try {
286                    elementBean.initResource(m_cms);
287                } catch (CmsException e) {
288                    // Skip elements whose resources can't be read
289                    LOG.warn(e.getLocalizedMessage(), e);
290                    continue;
291                }
292                ElementNode elemNode = new ElementNode(elementBean, containerNode);
293                if (elementBean.getInstanceId() != null) {
294                    elementsByInstanceId.put(elementBean.getInstanceId(), elemNode);
295                }
296                I_CmsFormatterBean formatter = getFormatter(m_cms, container, elementBean, adeConfig);
297                elementBean.initSettings(m_cms, adeConfig, formatter, Locale.ENGLISH, null, new HashMap<>());
298                containerNode.add(elemNode);
299            }
300        }
301
302        ElementNode rootElement = new ElementNode(null, null); // Dummy element containing all the top-level containers
303        for (ContainerNode containerNode : containerNodes) {
304            CmsContainerBean container = containerNode.getContainer();
305            String parentId = container.getParentInstanceId();
306            ElementNode parentElement = CmsStringUtil.isEmpty(parentId) ? null : elementsByInstanceId.get(parentId);
307            if (parentElement == null) {
308                rootElement.add(containerNode);
309            } else {
310                parentElement.add(containerNode);
311            }
312
313        }
314        return rootElement;
315
316    }
317
318    /**
319     * Renders the JSON for the container page.
320     *
321     * @return the JSON for the container page
322     * @throws Exception if something goes wrong
323     */
324    public Object renderJson() throws Exception {
325
326        CmsFile file = m_cms.readFile(m_page);
327        CmsXmlContainerPage page = CmsXmlContainerPageFactory.unmarshal(m_cms, file);
328        CmsContainerPageBean pageBean = page.getContainerPage(m_cms);
329        ElementNode root = buildTree(pageBean, file.getRootPath());
330        return elementToJson(root);
331    }
332
333    /**
334     * Renders a container node as JSON.
335     *
336     * @param containerNode the container node
337     * @return the JSON for the node
338     * @throws JSONException if something goes wrong with JSON processing
339     */
340    JSONObject containerToJson(ContainerNode containerNode) throws JSONException {
341
342        JSONObject result = new JSONObject();
343        result.put("name", containerNode.getName());
344        result.put("type", containerNode.getType());
345        result.put("isDetailOnlyContainer", containerNode.isDetailOnlyContainer());
346        result.put("isNestedContainer", containerNode.isNestedContainer());
347        result.put("isRootContainer", containerNode.isRootContainer());
348        JSONArray elemJson = new JSONArray();
349        for (ElementNode elemNode : containerNode.getElements()) {
350            elemJson.put(elementToJson(elemNode));
351        }
352        result.put("elements", elemJson);
353        return result;
354    }
355
356    /**
357     * Renders an element node as JSON.
358     *
359     * @param elementNode the element node
360     * @return the JSON for the element
361     * @throws JSONException if something goes wrong
362     */
363    JSONObject elementToJson(ElementNode elementNode) throws JSONException {
364
365        JSONObject result = new JSONObject();
366        if (elementNode.getElement() != null) {
367            result.put("path", elementNode.getElement().getResource().getRootPath());
368            // new container page format has property formatterKey
369            String formatterKey = CmsFormatterUtils.getFormatterKey(
370                elementNode.getParentContainerNode().getName(),
371                elementNode.getElement());
372            result.put("formatterKey", formatterKey);
373            JSONObject settings = new JSONObject();
374            for (Map.Entry<String, String> entry : elementNode.getElement().getSettings().entrySet()) {
375                // formatterSettings and element_instance_id setting have become obsolete in the new container page format
376                if (entry.getKey().startsWith("formatterSettings") || entry.getKey().equals("element_instance_id")) {
377                    continue;
378                }
379                settings.put(entry.getKey(), entry.getValue());
380            }
381            result.put("settings", settings);
382        }
383        JSONArray containers = new JSONArray();
384        for (ContainerNode containerNode : elementNode.getContainers().values()) {
385            containers.put(containerToJson(containerNode));
386        }
387        result.put("containers", containers);
388        return result;
389    }
390
391    /**
392     * Helper method for getting the formatter bean for a container element.
393     *
394     * <p>This only relies on the container element data itself since container width/type are not stored with the container,
395     * so it may return null if no formatter is set as an element setting.
396     *
397     * @param cms the CMS context
398     * @param container the container in which the element is located
399     * @param elementBean  the element bean
400     * @param adeConfig the ADE configuration
401     * @return the formatter bean
402     */
403    private I_CmsFormatterBean getFormatter(
404        CmsObject cms,
405        CmsContainerBean container,
406        CmsContainerElementBean elementBean,
407        CmsADEConfigData adeConfig) {
408
409        Collection<I_CmsFormatterBean> formatterList = adeConfig.getCachedFormatters().getFormattersForType(
410            OpenCms.getResourceManager().getResourceType(elementBean.getResource()).getTypeName(),
411            false);
412        Map<CmsUUID, I_CmsFormatterBean> formatters = new HashMap<>();
413        for (I_CmsFormatterBean formatter : formatterList) {
414            formatters.put(new CmsUUID(formatter.getId()), formatter);
415        }
416
417        Map<String, String> settings = elementBean.getIndividualSettings();
418        I_CmsFormatterBean result = null;
419
420        String forKeyWithContainer = settings.get(CmsFormatterConfig.FORMATTER_SETTINGS_KEY + container.getName());
421        String forKeyWithoutContainer = settings.get(CmsFormatterConfig.FORMATTER_SETTINGS_KEY);
422        for (String formatterId : new String[] {forKeyWithContainer, forKeyWithoutContainer}) {
423            if (CmsUUID.isValidUUID(formatterId)) {
424                result = formatters.get(new CmsUUID(formatterId));
425                break;
426            }
427        }
428        CmsUUID elementJspId = elementBean.getFormatterId();
429        if ((result == null) && (elementJspId != null)) {
430            LOG.warn(
431                "Formatter id not found for element "
432                    + elementBean.getResource().getRootPath()
433                    + "  in "
434                    + m_page.getRootPath());
435
436            for (I_CmsFormatterBean bean : formatters.values()) {
437                if (bean.getJspStructureId().equals(elementJspId)) {
438                    result = bean;
439                    break;
440                }
441            }
442        }
443        return result;
444
445    }
446
447}