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.configuration.CmsParameterConfiguration;
031import org.opencms.file.CmsObject;
032import org.opencms.json.JSONArray;
033import org.opencms.json.JSONException;
034import org.opencms.json.JSONObject;
035import org.opencms.main.CmsException;
036import org.opencms.main.OpenCms;
037import org.opencms.relations.CmsLink;
038import org.opencms.relations.I_CmsCustomLinkRenderer;
039import org.opencms.xml.content.CmsXmlContent;
040import org.opencms.xml.types.I_CmsXmlContentValue;
041import org.opencms.xml.xml2json.CmsJsonResourceHandler;
042import org.opencms.xml.xml2json.CmsXmlContentTree;
043import org.opencms.xml.xml2json.CmsXmlContentTree.Field;
044import org.opencms.xml.xml2json.CmsXmlContentTree.Node;
045import org.opencms.xml.xml2json.I_CmsJsonFormattableValue;
046import org.opencms.xml.xml2json.handler.CmsJsonHandlerContext;
047
048import java.util.AbstractMap.SimpleEntry;
049import java.util.List;
050import java.util.Locale;
051
052/**
053 * Converts an XML content to JSON by creating a CmsXmlContentTree and then recursively processing its nodes.
054 *
055 * <p>This specific renderer class does not need to be initialized with a CmsJsonHandlerContext, you can
056 * just initialize it with a CmsObject.
057 */
058public class CmsJsonRendererXmlContent implements I_CmsJsonRendererXmlContent {
059
060    /** The CMS context. */
061    private CmsObject m_cms;
062
063    /** The root Cms context. */
064    private CmsObject m_rootCms;
065
066    /**
067     * Creates a new instance.
068     *
069     * If this constructor is used, you still have to call one of the initialize() methods before rendering XML content to JSON.
070     */
071    public CmsJsonRendererXmlContent() {
072
073        // do nothing
074    }
075
076    /**
077     * Creates a new instance.
078     *
079     * @param cms the CMS context to use
080     * @throws CmsException if something goes wrong
081     */
082    public CmsJsonRendererXmlContent(CmsObject cms)
083    throws CmsException {
084
085        initialize(cms);
086    }
087
088    /**
089     * Builds a simple JSON object with link and path fields whose values are taken from the corresponding parameters.
090     *
091     * <p>If path is null, it will not be added to the result JSON.
092     *
093     * @param link the value for the link field
094     * @param path the value for the path field
095     * @return the link-and-path object
096     * @throws JSONException if something goes wrong
097     */
098    public static JSONObject linkAndPath(String link, String path, CmsObject cms) throws JSONException {
099
100        JSONObject result = new JSONObject();
101        result.put("link", link);
102        if (path != null) {
103            int paramPos = path.indexOf("?");
104            if (paramPos != -1) {
105                path = path.substring(0, paramPos);
106            }
107            path = OpenCms.getLinkManager().getRootPath(cms, path);
108            result.put("path", path);
109        }
110        return result;
111    }
112
113    /**
114     * Helper method to apply renderer to all locales of an XML content, and put the resulting objects into a JSON object with the locales as keys.
115     *
116     * @param content the content
117     * @param renderer the renderer to use
118     * @return the result JSON
119     * @throws JSONException if something goes wrong
120     */
121    public static JSONObject renderAllLocales(CmsXmlContent content, I_CmsJsonRendererXmlContent renderer)
122    throws JSONException {
123
124        List<Locale> locales = content.getLocales();
125        JSONObject result = new JSONObject(true);
126        for (Locale locale : locales) {
127            Object jsonForLocale = renderer.render(content, locale);
128            result.put(locale.toString(), jsonForLocale);
129        }
130        return result;
131    }
132
133    /**
134     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String)
135     */
136    public void addConfigurationParameter(String paramName, String paramValue) {
137
138        // do nothing
139    }
140
141    /**
142     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration()
143     */
144    public CmsParameterConfiguration getConfiguration() {
145
146        return null;
147    }
148
149    /**
150     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration()
151     */
152    public void initConfiguration() {
153
154        // do nothing
155    }
156
157    /**
158     * @see org.opencms.xml.xml2json.renderer.I_CmsJsonRendererXmlContent#initialize(org.opencms.xml.xml2json.handler.CmsJsonHandlerContext)
159     */
160    public void initialize(CmsJsonHandlerContext context) throws CmsException {
161
162        initialize(context.getCms());
163    }
164
165    /**
166     * Initializes the renderer.
167     *
168     * @param cms the CMS context to use
169     *
170     * @throws CmsException if something goes wrong
171     */
172    public void initialize(CmsObject cms) throws CmsException {
173
174        m_cms = OpenCms.initCmsObject(cms);
175        Object context = cms.getRequestContext().getAttribute(CmsJsonResourceHandler.ATTR_CONTEXT);
176        m_rootCms = OpenCms.initCmsObject(m_cms);
177        m_rootCms.getRequestContext().setSiteRoot("");
178        if (context != null) {
179            for (CmsObject currentCms : new CmsObject[] {m_cms, m_rootCms}) {
180                currentCms.getRequestContext().setAttribute(CmsJsonResourceHandler.ATTR_CONTEXT, context);
181            }
182        }
183        I_CmsCustomLinkRenderer linkRenderer = CmsJsonResourceHandler.getLinkRenderer(cms);
184        if (linkRenderer != null) {
185            m_cms.getRequestContext().setAttribute(CmsLink.CUSTOM_LINK_HANDLER, linkRenderer);
186        }
187    }
188
189    /**
190     * @see org.opencms.xml.xml2json.renderer.I_CmsJsonRendererXmlContent#render(org.opencms.xml.content.CmsXmlContent, java.util.Locale)
191     */
192    @Override
193    public Object render(CmsXmlContent content, Locale locale) throws JSONException {
194
195        CmsXmlContentTree tree = new CmsXmlContentTree(content, locale);
196        m_cms.getRequestContext().setLocale(locale);
197        m_rootCms.getRequestContext().setLocale(locale);
198        Node root = tree.getRoot();
199        return renderNode(root);
200
201    }
202
203    /**
204     * Renders a tree node as JSON.
205     *
206     * @param node the tree node
207     * @return the JSON (may be JSONObject, JSONArray, or String)
208     *
209     * @throws JSONException if something goes wrong
210     */
211    public Object renderNode(Node node) throws JSONException {
212
213        switch (node.getType()) {
214            case sequence:
215                List<Field> fields = node.getFields();
216                JSONObject result = new JSONObject(true);
217                for (Field field : fields) {
218                    SimpleEntry<String, Object> keyAndValue = renderField(field);
219                    if (keyAndValue != null) {
220                        result.put(keyAndValue.getKey(), keyAndValue.getValue());
221                    }
222                }
223                return result;
224            case choice:
225                JSONArray array = new JSONArray();
226                for (Field field : node.getFields()) {
227
228                    SimpleEntry<String, Object> keyAndValue = renderField(field);
229                    if (keyAndValue != null) {
230                        JSONObject choiceObj = new JSONObject(true);
231                        choiceObj.put(keyAndValue.getKey(), keyAndValue.getValue());
232                        array.put(choiceObj);
233                    }
234
235                }
236                return array;
237            case simple:
238                Object valueJson = renderSimpleValue(node);
239                return valueJson;
240            default:
241                throw new IllegalArgumentException("Unsupported node: " + node.getType());
242
243        }
244
245    }
246
247    /**
248     * Renders a tree field as a field in the given JSON object.
249     *
250     * @param field the field to render
251     * @return the key/value pair for the field
252     *
253     * @throws JSONException if something goes wrong
254     */
255    protected SimpleEntry<String, Object> renderField(Field field) throws JSONException {
256
257        String name = field.getName();
258        if (field.isMultivalue()) {
259            // If field is *potentially* multivalue,
260            // we always generate a JSON array for the sake of consistency,
261            // no matter how many actual values we currently have
262            JSONArray array = new JSONArray();
263            if (field.getFieldDefinition().isChoiceType()) {
264
265                // Multiple choice values can be represented by either a multivalued field of single-valued choices,
266                // or a single-valued field of multivalue choices. Using both in combination doesn't seem to make sense.
267                // So we collapse consecutive choice values in a multivalue field to give a uniform JSON syntax for the two
268                // cases to make the JSON easier to work with, but we lose some information about the structure of the XML.
269                for (Node subNode : field.getNodes()) {
270                    JSONArray choiceJson = (JSONArray)renderNode(subNode);
271                    array.append(choiceJson);
272                }
273            } else {
274                for (Node subNode : field.getNodes()) {
275                    array.put(renderNode(subNode));
276                }
277
278            }
279            return new SimpleEntry<>(name, array);
280        } else if (field.getNodes().size() == 1) {
281            if (field.getFieldDefinition().isChoiceType() && !field.isMultiChoice()) {
282                // field *and* choice single-valued, so we can unwrap the single value
283                JSONArray array = (JSONArray)renderNode(field.getNode());
284                if (array.length() == 1) {
285                    return new SimpleEntry<>(name, array.get(0));
286
287                }
288            } else {
289                return new SimpleEntry<>(name, renderNode(field.getNodes().get(0)));
290            }
291        }
292        return null;
293    }
294
295    /**
296     * Renders a simple value (i.e. not a nested content).
297     *
298     * @param node the node
299     * @return the JSON representation for the value
300     * @throws JSONException if something goes wrong
301     */
302    protected Object renderSimpleValue(Node node) throws JSONException {
303
304        I_CmsXmlContentValue value = node.getValue();
305        if (value instanceof I_CmsJsonFormattableValue) {
306            return ((I_CmsJsonFormattableValue)value).toJson(m_cms);
307        } else {
308            return node.getValue().getStringValue(m_cms);
309        }
310    }
311
312}