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