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}