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.IOException;
022import java.io.Serializable;
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 org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.ecm.core.api.DocumentModel;
039import org.nuxeo.ecm.core.api.DocumentModelFactory;
040import org.nuxeo.ecm.core.api.SortInfo;
041import org.nuxeo.ecm.core.query.sql.model.Literal;
042import org.nuxeo.ecm.platform.contentview.jsf.ContentViewLayout;
043import org.nuxeo.ecm.platform.contentview.jsf.ContentViewLayoutImpl;
044import org.nuxeo.ecm.platform.contentview.jsf.ContentViewState;
045import org.nuxeo.ecm.platform.contentview.jsf.ContentViewStateImpl;
046import org.nuxeo.ecm.platform.forms.layout.io.Base64;
047
048import net.sf.json.JSONArray;
049import net.sf.json.JSONException;
050import net.sf.json.JSONNull;
051import net.sf.json.JSONObject;
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     */
070    public static String toJSON(ContentViewState state, boolean encode) throws IOException {
071        if (state == null) {
072            return null;
073        }
074        if (log.isDebugEnabled()) {
075            log.debug("Encoding content view state: " + state);
076        }
077
078        // build json
079        JSONObject jsonObject = new JSONObject();
080        jsonObject.element("contentViewName", state.getContentViewName());
081        jsonObject.element("pageProviderName", state.getPageProviderName());
082        jsonObject.element("pageSize", state.getPageSize());
083        jsonObject.element("currentPage", state.getCurrentPage());
084
085        JSONArray jsonQueryParams = new JSONArray();
086        Object[] queryParams = state.getQueryParameters();
087        if (queryParams != null) {
088            // NXP-10347 + NXP-17544: serialize to String all params that will be serialized to String by
089            // NXQLQueryBuilder anyway, for consistency
090            List<Object> serParams = new ArrayList<Object>();
091            for (Object queryParam : queryParams) {
092                if (queryParam == null) {
093                    serParams.add(null);
094                } else if (queryParam instanceof Object[] || queryParam instanceof Collection
095                        || queryParam instanceof Boolean || queryParam instanceof Number
096                        || queryParam instanceof Literal) {
097                    serParams.add(queryParam);
098                } else {
099                    serParams.add(queryParam.toString());
100                }
101            }
102            jsonQueryParams.addAll(serParams);
103        }
104        jsonObject.element("queryParameters", jsonQueryParams);
105
106        jsonObject.element("searchDocument", getDocumentModelToJSON(state.getSearchDocumentModel()));
107
108        JSONArray jsonSortInfos = new JSONArray();
109        List<SortInfo> sortInfos = state.getSortInfos();
110        if (sortInfos != null) {
111            for (SortInfo sortInfo : sortInfos) {
112                jsonSortInfos.add(getSortInfoToJSON(sortInfo));
113            }
114        }
115        jsonObject.element("sortInfos", jsonSortInfos);
116
117        jsonObject.element("resultLayout", getContentViewLayoutToJSON(state.getResultLayout()));
118
119        List<String> resultColumns = state.getResultColumns();
120        if (resultColumns != null) {
121            jsonObject.element("resultColumns", resultColumns);
122        }
123
124        jsonObject.element("executed", state.isExecuted());
125
126        String jsonString = jsonObject.toString();
127
128        if (log.isDebugEnabled()) {
129            log.debug("Encoded content view state: " + jsonString);
130        }
131
132        // encoding
133        if (encode) {
134            String encodedValues = Base64.encodeBytes(jsonString.getBytes(), Base64.GZIP | Base64.DONT_BREAK_LINES);
135            jsonString = URLEncoder.encode(encodedValues, ENCODED_VALUES_ENCODING);
136        }
137        return jsonString;
138    }
139
140    /**
141     * Returns the content view state from its String serialization in JSON format.
142     *
143     * @param json the state to de-serialize
144     * @param decode if true, the input String is decoded from Base-64 format and unzipped.
145     */
146    @SuppressWarnings("unchecked")
147    public static ContentViewState fromJSON(String json, boolean decode) throws IOException {
148        if (json == null || json.trim().length() == 0) {
149            return null;
150        }
151        // decoding
152        if (decode) {
153            String decodedValues = URLDecoder.decode(json, ENCODED_VALUES_ENCODING);
154            json = new String(Base64.decode(decodedValues));
155        }
156
157        if (log.isDebugEnabled()) {
158            log.debug("Decoding content view state: " + json);
159        }
160
161        // parse json
162        JSONObject jsonObject = JSONObject.fromObject(json);
163        ContentViewState state = new ContentViewStateImpl();
164
165        state.setContentViewName(jsonObject.getString("contentViewName"));
166        state.setPageProviderName(jsonObject.optString("pageProviderName", null));
167        state.setPageSize(Long.valueOf(jsonObject.optLong("pageSize", -1)));
168        state.setCurrentPage(Long.valueOf(jsonObject.optLong("currentPage", -1)));
169
170        JSONArray jsonQueryParams = jsonObject.getJSONArray("queryParameters");
171        if (jsonQueryParams != null && !jsonQueryParams.isEmpty()) {
172            List<Object> queryParams = new ArrayList<Object>();
173            for (Object item : jsonQueryParams) {
174                if (item instanceof JSONNull) {
175                    queryParams.add(null);
176                } else if (item instanceof JSONArray) {
177                    queryParams.add(JSONArray.toCollection((JSONArray) item));
178                } else {
179                    queryParams.add(item);
180                }
181            }
182            state.setQueryParameters(queryParams.toArray(new Object[queryParams.size()]));
183        }
184
185        JSONObject jsonDoc = jsonObject.getJSONObject("searchDocument");
186        DocumentModel searchDoc = getDocumentModelFromJSON(jsonDoc);
187        state.setSearchDocumentModel(searchDoc);
188
189        JSONArray jsonSortInfos = jsonObject.getJSONArray("sortInfos");
190
191        if (jsonSortInfos != null && !jsonSortInfos.isEmpty()) {
192            List<SortInfo> sortInfos = new ArrayList<SortInfo>();
193            for (Object item : jsonSortInfos) {
194                sortInfos.add(getSortInfoFromJSON((JSONObject) item));
195            }
196            state.setSortInfos(sortInfos);
197        }
198
199        state.setResultLayout(getContentViewLayoutFromJSON(jsonObject.getJSONObject("resultLayout")));
200
201        JSONArray jsonResultColumns = jsonObject.optJSONArray("resultColumns");
202        if (jsonResultColumns != null) {
203            List<String> resultColumns = new ArrayList<String>();
204            resultColumns.addAll(jsonResultColumns);
205            state.setResultColumns(resultColumns);
206        }
207
208        state.setExecuted(jsonObject.optBoolean("executed"));
209
210        if (log.isDebugEnabled()) {
211            log.debug("Decoded content view state: " + state);
212        }
213
214        return state;
215    }
216
217    protected static JSONObject getSortInfoToJSON(SortInfo sortInfo) {
218        JSONObject res = new JSONObject();
219        res.element(SortInfo.SORT_COLUMN_NAME, sortInfo.getSortColumn());
220        res.element(SortInfo.SORT_ASCENDING_NAME, sortInfo.getSortAscending());
221        return res;
222    }
223
224    protected static SortInfo getSortInfoFromJSON(JSONObject jsonSortInfo) {
225        String sortColumn = jsonSortInfo.getString(SortInfo.SORT_COLUMN_NAME);
226        boolean sortAscending = jsonSortInfo.getBoolean(SortInfo.SORT_ASCENDING_NAME);
227        return new SortInfo(sortColumn, sortAscending);
228    }
229
230    protected static JSONObject getDocumentModelToJSON(DocumentModel doc) {
231        if (doc == null) {
232            return null;
233        }
234        JSONObject res = new JSONObject();
235        res.element("type", doc.getType());
236        JSONObject props = (new DocumentModelToJSON()).run(doc);
237        res.element("properties", props);
238        return res;
239    }
240
241    @SuppressWarnings("unchecked")
242    protected static DocumentModel getDocumentModelFromJSON(JSONObject jsonDoc) {
243        if (jsonDoc == null || jsonDoc.isNullObject()) {
244            return null;
245        }
246        String docType = jsonDoc.getString("type");
247        DocumentModel doc = DocumentModelFactory.createDocumentModel(docType);
248        JSONObject props = jsonDoc.getJSONObject("properties");
249        Iterator<String> keys = props.keys();
250        while (keys.hasNext()) {
251            String key = keys.next();
252            doc.setPropertyValue(key, getDocumentPropertyValue(props.get(key)));
253        }
254        return doc;
255    }
256
257    protected static JSONObject getContentViewLayoutToJSON(ContentViewLayout cvLayout) {
258        if (cvLayout == null) {
259            return null;
260        }
261        JSONObject res = new JSONObject();
262        res.element("name", cvLayout.getName());
263        res.element("title", cvLayout.getTitle());
264        res.element("translateTitle", cvLayout.getTranslateTitle());
265        res.element("iconPath", cvLayout.getIconPath());
266        res.element("showCSVExport", cvLayout.getShowCSVExport());
267        return res;
268    }
269
270    protected static ContentViewLayout getContentViewLayoutFromJSON(JSONObject jsonCvLayout) {
271        if (jsonCvLayout == null || jsonCvLayout.isNullObject()) {
272            return null;
273        }
274        String name = jsonCvLayout.optString("name", null);
275        String title = jsonCvLayout.optString("title", null);
276        boolean translateTitle = jsonCvLayout.optBoolean("translateTitle");
277        String iconPath = jsonCvLayout.optString("iconPath", null);
278        boolean showCSVExport = jsonCvLayout.optBoolean("showCSVExport");
279        return new ContentViewLayoutImpl(name, title, translateTitle, iconPath, showCSVExport);
280    }
281
282    @SuppressWarnings("unchecked")
283    protected static Serializable getDocumentPropertyValue(Object o) throws JSONException {
284        if (o == null || o instanceof JSONNull) {
285            return null;
286        } else if (o instanceof String) {
287            Calendar calendar = null;
288            try {
289                DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZ");
290                Date date = df.parse((String) o);
291                calendar = Calendar.getInstance();
292                calendar.setTime(date);
293            } catch (ParseException e) {
294            }
295
296            if (calendar != null) {
297                return calendar;
298            } else {
299                return (Serializable) o;
300            }
301        } else if (o instanceof JSONArray) {
302            JSONArray jsonArray = (JSONArray) o;
303            ArrayList<Serializable> list = new ArrayList<Serializable>();
304            for (Object aJsonArray : jsonArray) {
305                Serializable pValue = getDocumentPropertyValue(aJsonArray);
306                if (pValue != null) {
307                  list.add(pValue);
308                }
309            }
310            return list;
311        } else if (o instanceof JSONObject) {
312            JSONObject jsonObject = (JSONObject) o;
313            HashMap<String, Serializable> map = new HashMap<String, Serializable>();
314            Iterator<String> keys = jsonObject.keys();
315            while (keys.hasNext()) {
316                String key = keys.next();
317                map.put(key, getDocumentPropertyValue(jsonObject.get(key)));
318            }
319            return map;
320        } else {
321            return (Serializable) o;
322        }
323    }
324
325}