001/*
002 * (C) Copyright 2011 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Anahide Tchertchian
016 */
017package org.nuxeo.ecm.platform.contentview.json;
018
019import java.io.Serializable;
020import java.io.UnsupportedEncodingException;
021import java.net.URLDecoder;
022import java.net.URLEncoder;
023import java.text.DateFormat;
024import java.text.ParseException;
025import java.text.SimpleDateFormat;
026import java.util.ArrayList;
027import java.util.Calendar;
028import java.util.Collection;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.Iterator;
032import java.util.List;
033
034import net.sf.json.JSONArray;
035import net.sf.json.JSONException;
036import net.sf.json.JSONNull;
037import net.sf.json.JSONObject;
038
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.nuxeo.ecm.core.api.DocumentModel;
042import org.nuxeo.ecm.core.api.DocumentModelFactory;
043import org.nuxeo.ecm.core.api.SortInfo;
044import org.nuxeo.ecm.core.query.sql.model.Literal;
045import org.nuxeo.ecm.platform.contentview.jsf.ContentViewLayout;
046import org.nuxeo.ecm.platform.contentview.jsf.ContentViewLayoutImpl;
047import org.nuxeo.ecm.platform.contentview.jsf.ContentViewState;
048import org.nuxeo.ecm.platform.contentview.jsf.ContentViewStateImpl;
049import org.nuxeo.ecm.platform.forms.layout.io.Base64;
050
051/**
052 * Exporter/importer in JSON format of a {@link ContentViewState}.
053 *
054 * @since 5.4.2
055 */
056public class JSONContentViewState {
057
058    private static final Log log = LogFactory.getLog(JSONContentViewState.class);
059
060    public static final String ENCODED_VALUES_ENCODING = "UTF-8";
061
062    /**
063     * Returns the String serialization in JSON format of a content view state.
064     *
065     * @param state the state to serialize
066     * @param encode if true, the resulting String will be zipped and encoded in Base-64 format.
067     * @throws UnsupportedEncodingException
068     */
069    public static String toJSON(ContentViewState state, boolean encode) throws UnsupportedEncodingException {
070        if (state == null) {
071            return null;
072        }
073        if (log.isDebugEnabled()) {
074            log.debug("Encoding content view state: " + state);
075        }
076
077        // build json
078        JSONObject jsonObject = new JSONObject();
079        jsonObject.element("contentViewName", state.getContentViewName());
080        jsonObject.element("pageProviderName", state.getPageProviderName());
081        jsonObject.element("pageSize", state.getPageSize());
082        jsonObject.element("currentPage", state.getCurrentPage());
083
084        JSONArray jsonQueryParams = new JSONArray();
085        Object[] queryParams = state.getQueryParameters();
086        if (queryParams != null) {
087            // NXP-10347 + NXP-17544: serialize to String all params that will be serialized to String by
088            // NXQLQueryBuilder anyway, for consistency
089            List<Object> serParams = new ArrayList<Object>();
090            for (Object queryParam : queryParams) {
091                if (queryParam == null) {
092                    serParams.add(null);
093                } else if (queryParam instanceof Object[] || queryParam instanceof Collection
094                        || queryParam instanceof Boolean || queryParam instanceof Number
095                        || queryParam instanceof Literal) {
096                    serParams.add(queryParam);
097                } else {
098                    serParams.add(queryParam.toString());
099                }
100            }
101            jsonQueryParams.addAll(serParams);
102        }
103        jsonObject.element("queryParameters", jsonQueryParams);
104
105        jsonObject.element("searchDocument", getDocumentModelToJSON(state.getSearchDocumentModel()));
106
107        JSONArray jsonSortInfos = new JSONArray();
108        List<SortInfo> sortInfos = state.getSortInfos();
109        if (sortInfos != null) {
110            for (SortInfo sortInfo : sortInfos) {
111                jsonSortInfos.add(getSortInfoToJSON(sortInfo));
112            }
113        }
114        jsonObject.element("sortInfos", jsonSortInfos);
115
116        jsonObject.element("resultLayout", getContentViewLayoutToJSON(state.getResultLayout()));
117
118        List<String> resultColumns = state.getResultColumns();
119        if (resultColumns != null) {
120            jsonObject.element("resultColumns", resultColumns);
121        }
122
123        String jsonString = jsonObject.toString();
124
125        if (log.isDebugEnabled()) {
126            log.debug("Encoded content view state: " + jsonString);
127        }
128
129        // encoding
130        if (encode) {
131            String encodedValues = Base64.encodeBytes(jsonString.getBytes(), Base64.GZIP | Base64.DONT_BREAK_LINES);
132            jsonString = URLEncoder.encode(encodedValues, ENCODED_VALUES_ENCODING);
133        }
134        return jsonString;
135    }
136
137    /**
138     * Returns the content view state from its String serialization in JSON format.
139     *
140     * @param json the state to de-serialize
141     * @param decode if true, the input String is decoded from Base-64 format and unzipped.
142     * @throws UnsupportedEncodingException
143     */
144    @SuppressWarnings("unchecked")
145    public static ContentViewState fromJSON(String json, boolean decode) throws UnsupportedEncodingException {
146        if (json == null || json.trim().length() == 0) {
147            return null;
148        }
149        // decoding
150        if (decode) {
151            String decodedValues = URLDecoder.decode(json, ENCODED_VALUES_ENCODING);
152            json = new String(Base64.decode(decodedValues));
153        }
154
155        if (log.isDebugEnabled()) {
156            log.debug("Decoding content view state: " + json);
157        }
158
159        // parse json
160        JSONObject jsonObject = JSONObject.fromObject(json);
161        ContentViewState state = new ContentViewStateImpl();
162
163        state.setContentViewName(jsonObject.getString("contentViewName"));
164        state.setPageProviderName(jsonObject.optString("pageProviderName", null));
165        state.setPageSize(Long.valueOf(jsonObject.optLong("pageSize", -1)));
166        state.setCurrentPage(Long.valueOf(jsonObject.optLong("currentPage", -1)));
167
168        JSONArray jsonQueryParams = jsonObject.getJSONArray("queryParameters");
169        if (jsonQueryParams != null && !jsonQueryParams.isEmpty()) {
170            List<Object> queryParams = new ArrayList<Object>();
171            for (Object item : jsonQueryParams) {
172                if (item instanceof JSONNull) {
173                    queryParams.add(null);
174                } else if (item instanceof JSONArray) {
175                    queryParams.add(JSONArray.toCollection((JSONArray) item));
176                } else {
177                    queryParams.add(item);
178                }
179            }
180            state.setQueryParameters(queryParams.toArray(new Object[queryParams.size()]));
181        }
182
183        JSONObject jsonDoc = jsonObject.getJSONObject("searchDocument");
184        DocumentModel searchDoc = getDocumentModelFromJSON(jsonDoc);
185        state.setSearchDocumentModel(searchDoc);
186
187        JSONArray jsonSortInfos = jsonObject.getJSONArray("sortInfos");
188
189        if (jsonSortInfos != null && !jsonSortInfos.isEmpty()) {
190            List<SortInfo> sortInfos = new ArrayList<SortInfo>();
191            for (Object item : jsonSortInfos) {
192                sortInfos.add(getSortInfoFromJSON((JSONObject) item));
193            }
194            state.setSortInfos(sortInfos);
195        }
196
197        state.setResultLayout(getContentViewLayoutFromJSON(jsonObject.getJSONObject("resultLayout")));
198
199        JSONArray jsonResultColumns = jsonObject.optJSONArray("resultColumns");
200        if (jsonResultColumns != null) {
201            List<String> resultColumns = new ArrayList<String>();
202            resultColumns.addAll(jsonResultColumns);
203            state.setResultColumns(resultColumns);
204        }
205
206        if (log.isDebugEnabled()) {
207            log.debug("Decoded content view state: " + state);
208        }
209        return state;
210    }
211
212    protected static JSONObject getSortInfoToJSON(SortInfo sortInfo) {
213        JSONObject res = new JSONObject();
214        res.element(SortInfo.SORT_COLUMN_NAME, sortInfo.getSortColumn());
215        res.element(SortInfo.SORT_ASCENDING_NAME, sortInfo.getSortAscending());
216        return res;
217    }
218
219    protected static SortInfo getSortInfoFromJSON(JSONObject jsonSortInfo) {
220        String sortColumn = jsonSortInfo.getString(SortInfo.SORT_COLUMN_NAME);
221        boolean sortAscending = jsonSortInfo.getBoolean(SortInfo.SORT_ASCENDING_NAME);
222        return new SortInfo(sortColumn, sortAscending);
223    }
224
225    protected static JSONObject getDocumentModelToJSON(DocumentModel doc) {
226        if (doc == null) {
227            return null;
228        }
229        JSONObject res = new JSONObject();
230        res.element("type", doc.getType());
231        JSONObject props = (new DocumentModelToJSON()).run(doc);
232        res.element("properties", props);
233        return res;
234    }
235
236    @SuppressWarnings("unchecked")
237    protected static DocumentModel getDocumentModelFromJSON(JSONObject jsonDoc) {
238        if (jsonDoc == null || jsonDoc.isNullObject()) {
239            return null;
240        }
241        String docType = jsonDoc.getString("type");
242        DocumentModel doc = DocumentModelFactory.createDocumentModel(docType);
243        JSONObject props = jsonDoc.getJSONObject("properties");
244        Iterator<String> keys = props.keys();
245        while (keys.hasNext()) {
246            String key = keys.next();
247            doc.setPropertyValue(key, getDocumentPropertyValue(props.get(key)));
248        }
249        return doc;
250    }
251
252    protected static JSONObject getContentViewLayoutToJSON(ContentViewLayout cvLayout) {
253        if (cvLayout == null) {
254            return null;
255        }
256        JSONObject res = new JSONObject();
257        res.element("name", cvLayout.getName());
258        res.element("title", cvLayout.getTitle());
259        res.element("translateTitle", cvLayout.getTranslateTitle());
260        res.element("iconPath", cvLayout.getIconPath());
261        res.element("showCSVExport", cvLayout.getShowCSVExport());
262        return res;
263    }
264
265    protected static ContentViewLayout getContentViewLayoutFromJSON(JSONObject jsonCvLayout) {
266        if (jsonCvLayout == null || jsonCvLayout.isNullObject()) {
267            return null;
268        }
269        String name = jsonCvLayout.optString("name", null);
270        String title = jsonCvLayout.optString("title", null);
271        boolean translateTitle = jsonCvLayout.optBoolean("translateTitle");
272        String iconPath = jsonCvLayout.optString("iconPath", null);
273        boolean showCSVExport = jsonCvLayout.optBoolean("showCSVExport");
274        return new ContentViewLayoutImpl(name, title, translateTitle, iconPath, showCSVExport);
275    }
276
277    @SuppressWarnings("unchecked")
278    protected static Serializable getDocumentPropertyValue(Object o) throws JSONException {
279        if (o instanceof String) {
280            Calendar calendar = null;
281            try {
282                DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZ");
283                Date date = df.parse((String) o);
284                calendar = Calendar.getInstance();
285                calendar.setTime(date);
286            } catch (ParseException e) {
287            }
288
289            if (calendar != null) {
290                return calendar;
291            } else {
292                return (Serializable) o;
293            }
294        } else if (o instanceof JSONArray) {
295            JSONArray jsonArray = (JSONArray) o;
296            ArrayList<Serializable> list = new ArrayList<Serializable>();
297            for (Object aJsonArray : jsonArray) {
298                list.add(getDocumentPropertyValue(aJsonArray));
299            }
300            return list;
301        } else if (o instanceof JSONObject) {
302            JSONObject jsonObject = (JSONObject) o;
303            HashMap<String, Serializable> map = new HashMap<String, Serializable>();
304            Iterator<String> keys = jsonObject.keys();
305            while (keys.hasNext()) {
306                String key = keys.next();
307                map.put(key, getDocumentPropertyValue(jsonObject.get(key)));
308            }
309            return map;
310        } else {
311            return (Serializable) o;
312        }
313    }
314
315}