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