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.jsp.util;
029
030import org.opencms.json.JSONArray;
031import org.opencms.json.JSONException;
032import org.opencms.json.JSONObject;
033import org.opencms.util.CmsStringUtil;
034
035import java.util.AbstractCollection;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.Collections;
039import java.util.Iterator;
040import java.util.List;
041
042/**
043 * Wrapper for accessing JSON in JSPs.
044 */
045public class CmsJspJsonWrapper extends AbstractCollection<Object> {
046
047    /**
048     * Helper class representing a "handle" to a JSON object's entry through which it is possible to either access the entry's value or remove the entry.
049     */
050    private static class JSONObjectEntry {
051
052        /** The key for the entry. */
053        private String m_key;
054
055        /** The parent JSON object. */
056        private JSONObject m_object;
057
058        /**
059         * Creates a new instance.
060         *
061         * @param obj the parent JSON object
062         * @param key the key in the JSON object
063         */
064        public JSONObjectEntry(JSONObject obj, String key) {
065
066            m_object = obj;
067            m_key = key;
068        }
069
070        /**
071         * Helper method for getting the list of entries of a JSON object with the given key.
072         *
073         * <p>If the key just normally occurs in the given object, a wrapper for that entry will be returned.
074         * If the key is the wildcard '*', a list of all entries for the object is returned. If the key does not exist
075         * in the object, an empty list is returned.
076         *
077         * @param obj a JSON object
078         * @param key a JSON key (can be the wildcard '*')
079         *
080         * @return the list of entries for the given key
081         */
082        static List<JSONObjectEntry> getEntriesForKey(JSONObject obj, String key) {
083
084            List<JSONObjectEntry> result = new ArrayList<>();
085            if ("*".equals(key)) {
086                for (String actualKey : obj.keySet()) {
087                    result.add(new JSONObjectEntry(obj, actualKey));
088                }
089                return result;
090            } else {
091                Object child = obj.opt(key);
092                if (child != null) {
093                    return Collections.singletonList(new JSONObjectEntry(obj, key));
094                } else {
095                    return Collections.emptyList();
096                }
097            }
098        }
099
100        /**
101         * Gets the JSON key.
102         *
103         * @return the JSON key
104         */
105        public String getKey() {
106
107            return m_key;
108        }
109
110        /**
111         * Gets the entry's value.
112         *
113         * @return the value
114         */
115        public Object getValue() {
116
117            return m_object.opt(m_key);
118        }
119
120        /**
121         * Removes the entry from the parent JSON object.
122         */
123        public void remove() {
124
125            m_object.remove(m_key);
126        }
127    }
128
129    /** The wrapped value. */
130    private Object m_value;
131
132    /**
133     * Creates a new JSON wrapper.
134     *
135     * @param value the value to wrap
136     */
137    public CmsJspJsonWrapper(Object value) {
138
139        m_value = value;
140    }
141
142    /**
143     * Helper method for removing parts from a JSON object with a given path, which is already split into path components.
144     *
145     * See {@link #removePath(String)} for how the path works.
146     *
147     * @param obj the JSON object
148     * @param pathComponents the path components
149     */
150    public static void removePathInJson(Object obj, List<String> pathComponents) {
151
152        if (pathComponents.isEmpty()) {
153            return;
154        }
155        String key = pathComponents.get(0);
156        List<String> remainingPath = pathComponents.subList(1, pathComponents.size());
157        if (CmsStringUtil.isEmptyOrWhitespaceOnly(key)) {
158            removePathInJson(obj, remainingPath);
159            return;
160        }
161        if (obj instanceof JSONArray) {
162            JSONArray array = (JSONArray)obj;
163            for (int i = 0; i < array.length(); i++) {
164                removePathInJson(array.opt(i), pathComponents);
165            }
166        } else if (obj instanceof JSONObject) {
167            List<JSONObjectEntry> childrenForKey = JSONObjectEntry.getEntriesForKey((JSONObject)obj, key);
168            if (pathComponents.size() == 1) {
169                for (JSONObjectEntry child : childrenForKey) {
170                    child.remove();
171                }
172            } else {
173                for (JSONObjectEntry child : childrenForKey) {
174                    removePathInJson(child.getValue(), remainingPath);
175                }
176            }
177        }
178    }
179
180    /**
181     * Returns the JSON text as single line, that is as compact as possible.
182     *
183     * @return the JSON text
184     */
185    public String getCompact() {
186
187        try {
188            return JSONObject.valueToString(m_value);
189        } catch (JSONException e) {
190            throw new RuntimeException(e);
191        }
192    }
193
194    /**
195     * Returns the wrapped JSON object.
196     *
197     * This is an alias for {@link #getObject()}.
198     *
199     * @return the wrapped JSON object
200     *
201     * @see #getObject()
202     */
203    public Object getJson() {
204
205        return getObject();
206    }
207
208    /**
209     * Returns the wrapped JSON object.
210     *
211     * Useful in case you want to insert an existing JSON object into another JSON object.
212     *
213     * @return the wrapped JSON object
214     */
215    public Object getObject() {
216
217        return m_value;
218    }
219
220    /**
221     * Returns the JSON text in pretty-printed and indented format.
222     *
223     * @return the pretty-printed and indented JSON
224     */
225    public String getPretty() {
226
227        try {
228            return JSONObject.valueToString(m_value, 4, 0);
229        } catch (JSONException e) {
230            throw new RuntimeException(e);
231        }
232    }
233
234    /**
235     * Synonym for {@link #getPretty()}.
236     *
237     * @return the pretty-printed and indented JSON
238     *
239     * @see #getPretty()
240     */
241    public String getVerbose() {
242
243        return getPretty();
244    }
245
246    /**
247     * Supports the use of the <code>empty</code> operator in the JSP EL by implementing the Collection interface.<p>
248     *
249     * @see java.util.AbstractCollection#isEmpty()
250     */
251    @Override
252    public boolean isEmpty() {
253
254        if (m_value instanceof JSONObject) {
255            return ((JSONObject)m_value).length() < 1;
256        } else if (m_value instanceof JSONArray) {
257            return ((JSONArray)m_value).length() < 1;
258        } else if (m_value instanceof String) {
259            return CmsStringUtil.isEmptyOrWhitespaceOnly((String)m_value);
260        } else {
261            return m_value == null;
262        }
263    }
264
265    /**
266     * Supports the use of the <code>empty</code> operator in the JSP EL by implementing the Collection interface.<p>
267     *
268     * @return an empty Iterator in case {@link #isEmpty()} is <code>true</code>,
269     * otherwise an Iterator that will return the String value of this wrapper exactly once.<p>
270     *
271     * @see java.util.AbstractCollection#size()
272     */
273    @Override
274    public Iterator<Object> iterator() {
275
276        Iterator<Object> it = new Iterator<Object>() {
277
278            private boolean isFirst = true;
279
280            @Override
281            public boolean hasNext() {
282
283                return isFirst && !isEmpty();
284            }
285
286            @Override
287            public Object next() {
288
289                isFirst = false;
290                return getObject();
291            }
292
293            @Override
294            public void remove() {
295
296                throw new UnsupportedOperationException();
297            }
298        };
299        return it;
300    }
301
302    /**
303     * Removes the parts from the JSON object which match the given path.
304     *
305     * <p>
306     * The path is a slash-separated sequence of path components, where each path component is either the name of a JSON field, or the wildcard '*'.
307     * The removal process locates all JSON objects matching the parent path of the given path, and then removes the entry given by the last path component from it.
308     * <ul>
309     * <li>If a JSON array is encountered while descending the path, the rest of the path is processed for all elements of the array.
310     * <li>If an object is encountered which does not have an entry with the same name as the next path component, it and its contents are left unchanged.
311     * <li> The wildcard '* matches all keys in a JSON object.
312     * </ul>
313     *
314     * @param path the path which should be deleted
315     */
316    public void removePath(String path) {
317
318        path = path.trim();
319        removePathInJson(m_value, Arrays.asList(path.split("/")));
320
321    }
322
323    /**
324     * Supports the use of the <code>empty</code> operator in the JSP EL by implementing the Collection interface.<p>
325     *
326     * @return always returns 0.<p>
327     *
328     * @see java.util.AbstractCollection#size()
329     */
330    @Override
331    public int size() {
332
333        return isEmpty() ? 0 : 1;
334    }
335
336    /**
337     * @see java.lang.Object#toString()
338     */
339    @Override
340    public String toString() {
341
342        return getCompact();
343    }
344}