Java tutorial
/* ************************************************************************* * The contents of this file are subject to the Openbravo Public License * Version 1.1 (the "License"), being the Mozilla Public License * Version 1.1 with a permitted attribution clause; you may not use this * file except in compliance with the License. You may obtain a copy of * the License at http://www.openbravo.com/legal/license.html * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * The Original Code is Openbravo ERP. * The Initial Developer of the Original Code is Openbravo SLU * All portions are Copyright (C) 2009-2015 Openbravo SLU * All Rights Reserved. * Contributor(s): ______________________________________. ************************************************************************ */ package org.openbravo.service.json; import java.sql.BatchUpdateException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.enterprise.inject.Any; import javax.enterprise.inject.Instance; import javax.inject.Inject; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import org.hibernate.ScrollableResults; import org.openbravo.base.exception.OBException; import org.openbravo.base.model.Entity; import org.openbravo.base.model.ModelProvider; import org.openbravo.base.model.Property; import org.openbravo.base.provider.OBProvider; import org.openbravo.base.structure.BaseOBObject; import org.openbravo.base.util.Check; import org.openbravo.base.weld.WeldUtils; import org.openbravo.dal.core.DalUtil; import org.openbravo.dal.core.OBContext; import org.openbravo.dal.service.OBDal; import org.openbravo.database.SessionInfo; import org.openbravo.erpCommon.utility.OBMessageUtils; import org.openbravo.service.json.JsonToDataConverter.JsonConversionError; import org.openbravo.userinterface.selector.SelectorConstants; /** * Implements generic data operations which have parameters and json as an input and return results * as json strings. * * Note the parameters, json input and generated json follow the Smartclient specs. See the * Smartclient <a href="http://www.smartclient.com/docs/7.0rc2/a/b/c/go.html#class..RestDataSource"> * RestDataSource</a> for more information. * * The main usage of this class is through the {@link #getInstance()} method (as a singleton). This * class can however also be extended and instantiated directly. * * There are several methods to override/implement update and insert hooks, see the pre* and post* * methods. * * @author mtaal */ public class DefaultJsonDataService implements JsonDataService { private static final Logger log = Logger.getLogger(DefaultJsonDataService.class); private static final String ADD_FLAG = "_doingAdd"; private static DefaultJsonDataService instance = WeldUtils .getInstanceFromStaticBeanManager(DefaultJsonDataService.class); public static DefaultJsonDataService getInstance() { return instance; } public static void setInstance(DefaultJsonDataService instance) { DefaultJsonDataService.instance = instance; } /* * (non-Javadoc) * * @see org.openbravo.service.json.JsonDataService#fetch(java.util.Map) */ public String fetch(Map<String, String> parameters) { return fetch(parameters, true); } public String fetch(Map<String, String> parameters, boolean filterOnReadableOrganizations) { try { final String entityName = parameters.get(JsonConstants.ENTITYNAME); Check.isNotNull(entityName, "The name of the service/entityname should not be null"); Check.isNotNull(parameters, "The parameters should not be null"); doPreAction(parameters, "", DataSourceAction.FETCH); String selectedProperties = parameters.get(JsonConstants.SELECTEDPROPERTIES_PARAMETER); // The display property is present only for displaying table references in filter. // This parameter is used to set the identifier with the display column value. // Refer https://issues.openbravo.com/view.php?id=26696 String displayField = parameters.get(JsonConstants.DISPLAYFIELD_PARAMETER); if (StringUtils.isNotEmpty(displayField) && StringUtils.isNotEmpty(selectedProperties)) { boolean propertyPresent = false; for (String selectedProp : selectedProperties.split(",")) { if (selectedProp.equals(displayField)) { propertyPresent = true; break; } } if (!propertyPresent) { if (StringUtils.isNotEmpty(selectedProperties)) { selectedProperties = selectedProperties.concat("," + displayField); } else { selectedProperties = displayField; } } } final JSONObject jsonResult = new JSONObject(); final JSONObject jsonResponse = new JSONObject(); List<BaseOBObject> bobs; final String id = parameters.get(JsonConstants.ID); // if the id is set that's a special case of one object being requested if (id != null) { bobs = new ArrayList<BaseOBObject>(); final BaseOBObject bob = OBDal.getInstance().get(entityName, id); if (bob != null) { bobs.add(bob); } } else { final String startRowStr = parameters.get(JsonConstants.STARTROW_PARAMETER); final String endRowStr = parameters.get(JsonConstants.ENDROW_PARAMETER); boolean preventCountOperation = !parameters.containsKey(JsonConstants.NOCOUNT_PARAMETER) || "true".equals(parameters.get(JsonConstants.NOCOUNT_PARAMETER)); @SuppressWarnings("unchecked") Map<String, String> paramsCount = (Map<String, String>) ((HashMap<String, String>) parameters) .clone(); DataEntityQueryService queryService = createSetQueryService(paramsCount, true); queryService.setEntityName(entityName); // only do the count if a paging request is done and it has not been prevented // explicitly boolean doCount = false; int count = -1; int computedMaxResults = (queryService.getMaxResults() == null ? Integer.MAX_VALUE : queryService.getMaxResults()); if (startRowStr != null) { doCount = true; } if (endRowStr != null) { // note computedmaxresults must be set before // endRow is increased by 1 // increase by 1 to see if there are more results. if (preventCountOperation) { // set count here, is corrected in specific cases later count = queryService.getMaxResults(); } } else { // can't do count if there is no endrow... preventCountOperation = false; } if (doCount && !preventCountOperation) { count = queryService.count(); } if (parameters.containsKey(JsonConstants.ONLYCOUNT_PARAMETER)) { // stop here jsonResponse.put(JsonConstants.RESPONSE_TOTALROWS, count); return jsonResponse.toString(); } queryService = createSetQueryService(parameters, false, false, filterOnReadableOrganizations); if (parameters.containsKey(JsonConstants.SUMMARY_PARAMETER)) { final JSONObject singleResult = new JSONObject(); if (queryService.getSummaryFields().size() == 1) { singleResult.put(queryService.getSummaryFields().get(0), queryService.buildOBQuery().createQuery().uniqueResult()); } else { final Object[] os = (Object[]) queryService.buildOBQuery().createQuery().uniqueResult(); int i = 0; if (os != null && os.length > 0) { for (String key : queryService.getSummaryFields()) { singleResult.put(key, os[i++]); } } } singleResult.put("isGridSummary", true); jsonResponse.put(JsonConstants.RESPONSE_DATA, new JSONArray(Collections.singleton(singleResult))); jsonResponse.put(JsonConstants.RESPONSE_STATUS, JsonConstants.RPCREQUEST_STATUS_SUCCESS); jsonResult.put(JsonConstants.RESPONSE_RESPONSE, jsonResponse); jsonResponse.put(JsonConstants.RESPONSE_STARTROW, 0); jsonResponse.put(JsonConstants.RESPONSE_ENDROW, 1); jsonResponse.put(JsonConstants.RESPONSE_TOTALROWS, 1); return jsonResult.toString(); } else { String currentProfile = SessionInfo.getQueryProfile(); if (currentProfile == null || currentProfile.isEmpty()) { SessionInfo.setQueryProfile("grid"); } long t = System.currentTimeMillis(); bobs = queryService.list(); log.debug("query time:" + (System.currentTimeMillis() - t)); } bobs = bobFetchTransformation(bobs, parameters); // take start row from actual query service because it can be modified from the originally // requested one int startRow = queryService.getFirstResult() != null ? queryService.getFirstResult() : 0; if (preventCountOperation) { count = bobs.size() + startRow; // computedMaxResults is one too much, if we got one to much then correct // the result and up the count so that the grid knows that there are more if (bobs.size() == computedMaxResults) { bobs = bobs.subList(0, bobs.size() - 1); count++; } } jsonResponse.put(JsonConstants.RESPONSE_STARTROW, startRow); jsonResponse.put(JsonConstants.RESPONSE_ENDROW, (bobs.size() > 0 ? bobs.size() + startRow - 1 : 0)); // bobs can be empty and count > 0 if the order by forces a join without results if (bobs.isEmpty()) { if (startRow > 0) { // reload the startrow again from 0 parameters.put(JsonConstants.STARTROW_PARAMETER, "0"); parameters.put(JsonConstants.ENDROW_PARAMETER, computedMaxResults + ""); return fetch(parameters); } jsonResponse.put(JsonConstants.RESPONSE_TOTALROWS, 0); } else if (doCount) { jsonResponse.put(JsonConstants.RESPONSE_TOTALROWS, count); } } final DataToJsonConverter toJsonConverter = OBProvider.getInstance().get(DataToJsonConverter.class); toJsonConverter.setAdditionalProperties(JsonUtils.getAdditionalProperties(parameters)); toJsonConverter.setSelectedProperties(selectedProperties); if (StringUtils.isNotEmpty(displayField) && (!displayField.equals(JsonConstants.IDENTIFIER))) { toJsonConverter.setDisplayProperty(displayField); } final List<JSONObject> jsonObjects = toJsonConverter.toJsonObjects(bobs); addWritableAttribute(jsonObjects); jsonResponse.put(JsonConstants.RESPONSE_DATA, new JSONArray(jsonObjects)); jsonResponse.put(JsonConstants.RESPONSE_STATUS, JsonConstants.RPCREQUEST_STATUS_SUCCESS); jsonResult.put(JsonConstants.RESPONSE_RESPONSE, jsonResponse); return doPostAction(parameters, jsonResult.toString(), DataSourceAction.FETCH, null); } catch (Throwable t) { log.error(t.getMessage(), t); return JsonUtils.convertExceptionToJson(t); } } public void fetch(Map<String, String> parameters, QueryResultWriter writer) { long t = System.currentTimeMillis(); doPreAction(parameters, "", DataSourceAction.FETCH); final DataEntityQueryService queryService = createSetQueryService(parameters, false); String selectedProperties = parameters.get(JsonConstants.SELECTEDPROPERTIES_PARAMETER); final DataToJsonConverter toJsonConverter = OBProvider.getInstance().get(DataToJsonConverter.class); toJsonConverter.setAdditionalProperties(JsonUtils.getAdditionalProperties(parameters)); // Convert to Json only the properties specified in the request. If no properties are specified, // all of them will be converted to Json toJsonConverter.setSelectedProperties(selectedProperties); final ScrollableResults scrollableResults = queryService.scroll(); try { int i = 0; while (scrollableResults.next()) { final Object result = scrollableResults.get()[0]; final JSONObject json = toJsonConverter.toJsonObject((BaseOBObject) result, DataResolvingMode.FULL); try { doPostFetch(parameters, json); } catch (JSONException e) { throw new OBException(e); } writer.write(json); i++; // Clear session every 1000 records to prevent huge memory consumption in case of big loops if (i % 1000 == 0) { OBDal.getInstance().getSession().clear(); log.debug("clearing in record " + i + " elapsed time " + (System.currentTimeMillis() - t)); } } } finally { scrollableResults.close(); } log.debug("Fetch took " + (System.currentTimeMillis() - t) + " ms"); } protected DataEntityQueryService createSetQueryService(Map<String, String> parameters, boolean forCountOperation) { return createSetQueryService(parameters, forCountOperation, false, true); } private DataEntityQueryService createSetQueryService(Map<String, String> parameters, boolean forCountOperation, boolean forSubEntity, boolean filterOnReadableOrganizations) { boolean hasSubentity = false; String entityName = parameters.get(JsonConstants.ENTITYNAME); final DataEntityQueryService queryService = OBProvider.getInstance().get(DataEntityQueryService.class); if (!forSubEntity && parameters.get(JsonConstants.DISTINCT_PARAMETER) != null) { // this is the main entity of a 'contains' (used in FK drop down lists), it will create also // info for subentity final String distinctPropertyPath = parameters.get(JsonConstants.DISTINCT_PARAMETER); final Property distinctProperty = DalUtil .getPropertyFromPath(ModelProvider.getInstance().getEntity(entityName), distinctPropertyPath); final Entity distinctEntity = distinctProperty.getTargetEntity(); // criteria needs to be split in two parts: // -One for main entity (the one directly queried for) // -Another one for subentity String baseCriteria = ""; String subCriteria = ""; hasSubentity = true; if (!StringUtils.isEmpty(parameters.get("criteria"))) { String criteria = parameters.get("criteria"); for (String criterion : criteria.split(JsonConstants.IN_PARAMETER_SEPARATOR)) { try { JSONObject jsonCriterion = new JSONObject(criterion); if (jsonCriterion.getString("fieldName") .equals(distinctPropertyPath + "$" + JsonConstants.IDENTIFIER)) { jsonCriterion.put("fieldName", JsonConstants.IDENTIFIER); baseCriteria = jsonCriterion.toString(); } else { subCriteria += subCriteria.length() > 0 ? JsonConstants.IN_PARAMETER_SEPARATOR : ""; subCriteria += criterion; } } catch (JSONException e) { log.error("Error obtaining 'distint' criterion for " + criterion, e); } } } // params for subentity are based on main entity ones @SuppressWarnings("unchecked") Map<String, String> paramSubCriteria = (Map<String, String>) ((HashMap<String, String>) parameters) .clone(); // set proper criteria for each case if (StringUtils.isEmpty(subCriteria)) { paramSubCriteria.remove("criteria"); } else { paramSubCriteria.put("criteria", subCriteria); } if (StringUtils.isEmpty(baseCriteria)) { parameters.remove("criteria"); } else { parameters.put("criteria", baseCriteria); } // where parameter is only applied in subentity, remove it from main entity if (parameters.containsKey(JsonConstants.WHERE_PARAMETER)) { parameters.remove(JsonConstants.WHERE_PARAMETER); } // main entity ("me") settings queryService.getQueryBuilder().setMainAlias("me"); queryService.setEntityName(distinctEntity.getName()); queryService.setFilterOnReadableClients(false); queryService.setFilterOnReadableOrganizations(false); queryService.setFilterOnActive(false); // create now subentity queryService.setSubEntity(entityName, createSetQueryService(paramSubCriteria, forCountOperation, true, filterOnReadableOrganizations), distinctProperty, distinctPropertyPath); } else { queryService.setEntityName(entityName); queryService.setFilterOnReadableOrganizations(filterOnReadableOrganizations); if (parameters.containsKey(JsonConstants.USE_ALIAS)) { queryService.setUseAlias(); } } final String startRowStr = parameters.get(JsonConstants.STARTROW_PARAMETER); final String endRowStr = parameters.get(JsonConstants.ENDROW_PARAMETER); final JSONObject criteria = JsonUtils.buildCriteria(parameters); if ((StringUtils.isEmpty(startRowStr) || StringUtils.isEmpty(endRowStr)) && !isIDCriteria(criteria) && !parameters.containsKey("exportAs")) { // pagination is not set, this is most likely a bug log.warn("Fetching data without pagination, this can cause perfomance issues. Parameters: " + convertParameterToString(parameters)); if (parameters.containsKey(JsonConstants.TAB_PARAMETER) || parameters.containsKey(SelectorConstants.DS_REQUEST_SELECTOR_ID_PARAMETER)) { // for standard tab and selector datasources pagination is mandatory throw new OBException(OBMessageUtils.messageBD("OBJSON_NoPagedFetch")); } } boolean directNavigation = parameters.containsKey("_directNavigation") && "true".equals(parameters.get("_directNavigation")) && parameters.containsKey(JsonConstants.TARGETRECORDID_PARAMETER); if (parameters.containsKey(JsonConstants.TARGETRECORDID_PARAMETER) && parameters.get(JsonConstants.TARGETRECORDID_PARAMETER) != null && !"null".equals(parameters.get(JsonConstants.TARGETRECORDID_PARAMETER)) && !"true".equals(parameters.get("_directNavigation"))) { log.warn( "Datasource request with targetRecordId but without directNavigation detected. This type of requests should be avoided because they result in a query that performs poorly. Parameters: " + convertParameterToString(parameters)); } if (!directNavigation) { // set the where/org filter parameters and the @ parameters for (String key : parameters.keySet()) { if (key.equals(JsonConstants.WHERE_PARAMETER) || key.equals(JsonConstants.IDENTIFIER) || key.equals(JsonConstants.ORG_PARAMETER) || key.equals(JsonConstants.TARGETRECORDID_PARAMETER) || (key.startsWith(DataEntityQueryService.PARAM_DELIMITER) && key.endsWith(DataEntityQueryService.PARAM_DELIMITER))) { queryService.addFilterParameter(key, parameters.get(key)); } } } queryService.setCriteria(criteria); if (parameters.get(JsonConstants.NO_ACTIVE_FILTER) != null && parameters.get(JsonConstants.NO_ACTIVE_FILTER).equals("true")) { queryService.setFilterOnActive(false); } if (parameters.containsKey(JsonConstants.TEXTMATCH_PARAMETER_OVERRIDE)) { queryService.setTextMatching(parameters.get(JsonConstants.TEXTMATCH_PARAMETER_OVERRIDE)); } else { queryService.setTextMatching(parameters.get(JsonConstants.TEXTMATCH_PARAMETER)); } // only do the count if a paging request is done // note preventCountOperation variable is considered further below int startRow = 0; int computedMaxResults = Integer.MAX_VALUE; if (startRowStr != null) { startRow = Integer.parseInt(startRowStr); queryService.setFirstResult(startRow); } if (endRowStr != null) { int endRow = Integer.parseInt(endRowStr); computedMaxResults = endRow - startRow + 1; queryService.setMaxResults(computedMaxResults); } String orderBy = ""; if (!hasSubentity) { final String sortBy = parameters.get(JsonConstants.SORTBY_PARAMETER); if (sortBy != null) { orderBy = sortBy; } else if (parameters.get(JsonConstants.ORDERBY_PARAMETER) != null) { orderBy = parameters.get(JsonConstants.ORDERBY_PARAMETER); } if (parameters.get(JsonConstants.SUMMARY_PARAMETER) != null && parameters.get(JsonConstants.SUMMARY_PARAMETER).trim().length() > 0) { queryService.setSummarySettings(parameters.get(JsonConstants.SUMMARY_PARAMETER)); } else { // Always append id to the orderby to make a predictable sorting orderBy += (orderBy.isEmpty() ? "" : ",") + "id"; } } else { orderBy = JsonConstants.IDENTIFIER; } queryService.setOrderBy(orderBy); // compute a new startrow if the targetrecordid was passed in int targetRowNumber = -1; if (!forCountOperation && !directNavigation && parameters.containsKey(JsonConstants.TARGETRECORDID_PARAMETER)) { final String targetRecordId = parameters.get(JsonConstants.TARGETRECORDID_PARAMETER); targetRowNumber = queryService.getRowNumber(targetRecordId); if (targetRowNumber != -1) { startRow = targetRowNumber; // if the startrow is really low, then just read from 0 // to make sure that we have a full page of data to display if (startRow < (computedMaxResults / 2)) { startRow = 0; } else { startRow -= 20; } queryService.setFirstResult(startRow); } queryService.clearCachedValues(); } if (!forCountOperation) { queryService.setAdditionalProperties(JsonUtils.getAdditionalProperties(parameters)); // joining associated entities actually proved to be slower than doing // individual queries for them... so disabling this functionality for now // queryService.setJoinAssociatedEntities(true); } return queryService; } // Given a map of parameters, returns a string with the pairs key:value private String convertParameterToString(Map<String, String> parameters) { String paramMsg = ""; for (String paramKey : parameters.keySet()) { paramMsg += paramKey + ":" + parameters.get(paramKey) + "\n"; } return paramMsg; } private void addWritableAttribute(List<JSONObject> jsonObjects) throws JSONException { for (JSONObject jsonObject : jsonObjects) { if (!jsonObject.has("client") || !jsonObject.has("organization")) { continue; } final Object rowClient = jsonObject.get("client"); final Object rowOrganization = jsonObject.get("organization"); if (!(rowClient instanceof String) || !(rowOrganization instanceof String)) { continue; } final String currentClientId = OBContext.getOBContext().getCurrentClient().getId(); if (!rowClient.equals(currentClientId)) { jsonObject.put("_readOnly", true); } else { boolean writable = false; for (String orgId : OBContext.getOBContext().getWritableOrganizations()) { if (orgId.equals(rowOrganization)) { writable = true; break; } } if (!writable) { jsonObject.put("_readOnly", true); } } } } /* * (non-Javadoc) * * @see org.openbravo.service.json.JsonDataService#remove(java.util.Map) */ public String remove(Map<String, String> parameters) { final String id = parameters.get(JsonConstants.ID); if (id == null) { return JsonUtils.convertExceptionToJson(new IllegalStateException("No id parameter")); } final String entityName = parameters.get(JsonConstants.ENTITYNAME); if (entityName == null) { return JsonUtils.convertExceptionToJson(new IllegalStateException("No entityName parameter")); } BaseOBObject bob = OBDal.getInstance().get(entityName, id); if (bob != null) { try { // create the result info before deleting to prevent Hibernate errors final DataToJsonConverter toJsonConverter = OBProvider.getInstance().get(DataToJsonConverter.class); final List<JSONObject> jsonObjects = toJsonConverter.toJsonObjects(Collections.singletonList(bob)); final JSONObject jsonResult = new JSONObject(); final JSONObject jsonResponse = new JSONObject(); jsonResponse.put(JsonConstants.RESPONSE_STATUS, JsonConstants.RPCREQUEST_STATUS_SUCCESS); jsonResponse.put(JsonConstants.RESPONSE_DATA, new JSONArray(jsonObjects)); jsonResult.put(JsonConstants.RESPONSE_RESPONSE, jsonResponse); OBDal.getInstance().commitAndClose(); doPreAction(parameters, jsonObjects.toString(), DataSourceAction.REMOVE); // now do the real delete in a separate transaction // to prevent side effects that a child can not be deleted // from its parent // https://issues.openbravo.com/view.php?id=21229 bob = OBDal.getInstance().get(entityName, id); OBDal.getInstance().remove(bob); final String result = doPostAction(parameters, jsonResult.toString(), DataSourceAction.REMOVE, null); OBDal.getInstance().commitAndClose(); return result; } catch (Throwable t) { return JsonUtils.convertExceptionToJson(t); } } else { return JsonUtils.convertExceptionToJson(new IllegalStateException("Object not found")); } } /* * (non-Javadoc) * * @see org.openbravo.service.json.JsonDataService#add(java.util.Map, java.lang.String) */ public String add(Map<String, String> parameters, String content) { parameters.put(ADD_FLAG, "true"); return update(parameters, content); } /* * (non-Javadoc) * * @see org.openbravo.service.json.JsonDataService#update(java.util.Map, java.lang.String) */ public String update(Map<String, String> parameters, String content) { try { final boolean sendOriginalIdBack = "true".equals(parameters.get(JsonConstants.SEND_ORIGINAL_ID_BACK)); final JsonToDataConverter fromJsonConverter = OBProvider.getInstance().get(JsonToDataConverter.class); String localContent = content; if (parameters.containsKey(ADD_FLAG)) { localContent = doPreAction(parameters, content, DataSourceAction.ADD); } else { localContent = doPreAction(parameters, content, DataSourceAction.UPDATE); } final Object jsonContent = getContentAsJSON(localContent); final List<BaseOBObject> bobs; final List<JSONObject> originalData = new ArrayList<JSONObject>(); if (jsonContent instanceof JSONArray) { bobs = fromJsonConverter.toBaseOBObjects((JSONArray) jsonContent); final JSONArray jsonArray = (JSONArray) jsonContent; for (int i = 0; i < jsonArray.length(); i++) { originalData.add(jsonArray.getJSONObject(i)); } } else { final JSONObject jsonObject = (JSONObject) jsonContent; originalData.add(jsonObject); // now set the id and entityname from the parameters if it was set if (!jsonObject.has(JsonConstants.ID) && parameters.containsKey(JsonConstants.ID)) { jsonObject.put(JsonConstants.ID, parameters.containsKey(JsonConstants.ID)); } if (!jsonObject.has(JsonConstants.ENTITYNAME) && parameters.containsKey(JsonConstants.ENTITYNAME)) { jsonObject.put(JsonConstants.ENTITYNAME, parameters.get(JsonConstants.ENTITYNAME)); } bobs = Collections.singletonList(fromJsonConverter.toBaseOBObject((JSONObject) jsonContent)); } if (fromJsonConverter.hasErrors()) { OBDal.getInstance().rollbackAndClose(); // report the errors final JSONObject jsonResult = new JSONObject(); final JSONObject jsonResponse = new JSONObject(); jsonResponse.put(JsonConstants.RESPONSE_STATUS, JsonConstants.RPCREQUEST_STATUS_VALIDATION_ERROR); final JSONObject errorsObject = new JSONObject(); for (JsonConversionError error : fromJsonConverter.getErrors()) { errorsObject.put(error.getProperty().getName(), error.getThrowable().getMessage()); } jsonResponse.put(JsonConstants.RESPONSE_ERRORS, errorsObject); jsonResult.put(JsonConstants.RESPONSE_RESPONSE, jsonResponse); return jsonResult.toString(); } else { for (BaseOBObject bob : bobs) { OBDal.getInstance().save(bob); } OBDal.getInstance().flush(); // business event handlers can change the data // flush again before refreshing, refreshing can // potentially remove any in-memory changes int countFlushes = 0; while (OBDal.getInstance().getSession().isDirty()) { OBDal.getInstance().flush(); countFlushes++; // arbitrary point to give up... if (countFlushes > 100) { throw new OBException("Infinite loop in flushing when persisting json: " + content); } } // refresh the objects from the db as they can have changed for (BaseOBObject bob : bobs) { OBDal.getInstance().getSession().refresh(bob); // if object has computed columns refresh from the database too if (bob.getEntity().hasComputedColumns()) { OBDal.getInstance().getSession().refresh(bob.get(Entity.COMPUTED_COLUMNS_PROXY_PROPERTY)); } } // almost successfull, now create the response // needs to be done before the close of the session final DataToJsonConverter toJsonConverter = OBProvider.getInstance().get(DataToJsonConverter.class); toJsonConverter.setAdditionalProperties(JsonUtils.getAdditionalProperties(parameters)); final List<JSONObject> jsonObjects = toJsonConverter.toJsonObjects(bobs); if (sendOriginalIdBack) { // now it is assumed that the jsonObjects are the same size and the same location // in the array if (jsonObjects.size() != originalData.size()) { throw new OBException("Unequal sizes in json data processed " + jsonObjects.size() + " " + originalData.size()); } // now add the old id back for (int i = 0; i < originalData.size(); i++) { final JSONObject original = originalData.get(i); final JSONObject ret = jsonObjects.get(i); if (original.has(JsonConstants.ID) && original.has(JsonConstants.NEW_INDICATOR)) { ret.put(JsonConstants.ORIGINAL_ID, original.get(JsonConstants.ID)); } } } final JSONObject jsonResult = new JSONObject(); final JSONObject jsonResponse = new JSONObject(); jsonResponse.put(JsonConstants.RESPONSE_STATUS, JsonConstants.RPCREQUEST_STATUS_SUCCESS); jsonResponse.put(JsonConstants.RESPONSE_DATA, new JSONArray(jsonObjects)); jsonResult.put(JsonConstants.RESPONSE_RESPONSE, jsonResponse); final String result; if (parameters.containsKey(ADD_FLAG)) { result = doPostAction(parameters, jsonResult.toString(), DataSourceAction.ADD, content); } else { result = doPostAction(parameters, jsonResult.toString(), DataSourceAction.UPDATE, content); } OBDal.getInstance().commitAndClose(); return result; } } catch (Throwable t) { Throwable localThrowable = t; if (localThrowable.getCause() instanceof BatchUpdateException) { final BatchUpdateException batchException = (BatchUpdateException) localThrowable.getCause(); localThrowable = batchException.getNextException(); } log.error(localThrowable.getMessage(), localThrowable); return JsonUtils.convertExceptionToJson(localThrowable); } } private Object getContentAsJSON(String content) throws JSONException { Check.isNotNull(content, "Content must be set"); final Object jsonRepresentation; if (content.trim().startsWith("[")) { jsonRepresentation = new JSONArray(content); } else { final JSONObject jsonObject = new JSONObject(content); jsonRepresentation = jsonObject.get(JsonConstants.DATA); } return jsonRepresentation; } public static abstract class QueryResultWriter { public abstract void write(JSONObject json); } protected List<BaseOBObject> bobFetchTransformation(List<BaseOBObject> bobs, Map<String, String> parameters) { // If is override, take into account: // * If the number of the returned bobs change, there could be problems because endRow and // totalRows parameters will be out-of-sync with that the requester expects, and some values can // be missing in the following fetches. If there is no pagination (all values are returned at // once), there is no problem. // * If any bob is modified, the original entity is being modified, so a good practice could be // clone the bob (using DalUtil.copy, for example) before modify it, and then return the clone. return bobs; } /** * Hooks executed at the end of doPreAction and doPostAction to modify or to validate DataService * calls. */ @Inject @Any private Instance<JsonDataServiceExtraActions> extraActions; protected String doPreAction(Map<String, String> parameters, String content, DataSourceAction action) { try { if (action == DataSourceAction.FETCH) { // In fetch operations there is no data. Just call doPreFetch and extraActions. doPreFetch(parameters); for (JsonDataServiceExtraActions extraAction : extraActions) { extraAction.doPreAction(parameters, new JSONArray(), action); } return ""; } final Object contentObject = getContentAsJSON(content); final boolean isArray = contentObject instanceof JSONArray; final JSONArray data; if (isArray) { data = (JSONArray) contentObject; } else { final JSONObject request = new JSONObject(content); data = new JSONArray(Collections.singleton(request.getJSONObject(JsonConstants.DATA))); } final JSONArray newData = new JSONArray(); for (int i = 0; i < data.length(); i++) { final JSONObject dataElement = data.getJSONObject(i); // do the pre thing switch (action) { case UPDATE: doPreUpdate(parameters, dataElement); break; case ADD: doPreInsert(parameters, dataElement); break; case REMOVE: doPreRemove(parameters, dataElement); break; default: throw new OBException("Unsupported action " + action); } // and set it in the new array newData.put(dataElement); } for (JsonDataServiceExtraActions extraAction : extraActions) { extraAction.doPreAction(parameters, newData, action); } // return the array directly if (isArray) { return newData.toString(); } final JSONObject request = new JSONObject(content); request.put(JsonConstants.DATA, newData.getJSONObject(0)); return request.toString(); } catch (JSONException e) { throw new OBException(e); } finally { if (DataSourceAction.FETCH != action) { // Only flush non fetch operations. OBDal.getInstance().flush(); } } } protected String doPostAction(Map<String, String> parameters, String content, DataSourceAction action, String originalObject) { // Clear session to prevent slow flush if a fetch is done if (action.name().equals("FETCH")) { OBDal.getInstance().getSession().clear(); } OBDal.getInstance().flush(); try { // this gets the data before the insert, so that it can be used // for preprocessing, for example inserting an order final JSONObject json = new JSONObject(content); final JSONObject response = json.getJSONObject(JsonConstants.RESPONSE_RESPONSE); final JSONArray data = response.getJSONArray(JsonConstants.RESPONSE_DATA); final JSONArray newData = new JSONArray(); for (int i = 0; i < data.length(); i++) { final JSONObject dataElement = data.getJSONObject(i); // do the pre thing switch (action) { case FETCH: doPostFetch(parameters, dataElement); break; case UPDATE: doPostUpdate(parameters, dataElement, originalObject); break; case ADD: doPostInsert(parameters, dataElement, originalObject); break; case REMOVE: doPostRemove(parameters, dataElement); break; default: throw new OBException("Unsupported action " + action); } // and set it in the new array newData.put(dataElement); } // update the response with the changes, make it a string response.put(JsonConstants.RESPONSE_DATA, newData); json.put(JsonConstants.RESPONSE_RESPONSE, response); for (JsonDataServiceExtraActions extraAction : extraActions) { extraAction.doPostAction(parameters, json, action, originalObject); } return json.toString(); } catch (JSONException e) { throw new OBException(e); } } /** * Is called before the actual remove of the object. The toRemove object contains the id and * entity name of the to-be-deleted object. */ protected void doPreRemove(Map<String, String> parameters, JSONObject toRemove) throws JSONException { } /** * Is called after the remove in the database but before the commit. The removed parameter object * can be changed, the changes are sent to the client. This method is called in the same * transaction as the remove action. */ protected void doPostRemove(Map<String, String> parameters, JSONObject removed) throws JSONException { } /** * Is called before fetching an object. This method is called in the same transaction as the main * fetch operation. */ protected void doPreFetch(Map<String, String> parameters) throws JSONException { } /** * Is called after fetching an object before the result is sent to the client, the fetched * {@link JSONObject} can be changed. The changes are sent to the client. This method is called in * the same transaction as the main fetch operation. */ protected void doPostFetch(Map<String, String> parameters, JSONObject fetched) throws JSONException { } /** * Is called before an object is inserted. The toInsert {@link JSONObject} can be changed, the * changes are persisted to the database. This method is called in the same transaction as the * insert. */ protected void doPreInsert(Map<String, String> parameters, JSONObject toInsert) throws JSONException { } /** * Is called after the insert action in the same transaction as the insert. The inserted * {@link JSONObject} can be changed, the changes are sent to the client. * * The originalToInsert contains the json object/array string as it was passed into the * doPreInsert method. The inserted JSONObject is the object read from the database after it was * inserted. So it contains the changes done by stored procedures. */ protected void doPostInsert(Map<String, String> parameters, JSONObject inserted, String originalToInsert) throws JSONException { // final String id = inserted.getString(JsonConstants.ID); // final String entityName = inserted.getString(JsonConstants.ENTITYNAME); } /** * Called before the update of an object. Is called in the same transaction as the main update * operation. Changes to the toUpdate {@link JSONObject} are persisted in the database. */ protected void doPreUpdate(Map<String, String> parameters, JSONObject toUpdate) throws JSONException { } /** * Called after the updates have been done, within the same transaction as the main update. * Changes to the updated {@link JSONObject} are sent to the client (but not persisted to the * database). * * The originalToUpdate contains the json object/array string as it was passed into the * doPreUpdate method. The updated JSONObject is the object read from the database after it was * updated. So it contains all the changes done by stored procedures. */ protected void doPostUpdate(Map<String, String> parameters, JSONObject updated, String originalToUpdate) throws JSONException { } public enum DataSourceAction { FETCH, ADD, UPDATE, REMOVE } /** * Checks whether a criteria is filtering by ID property * * @param jsonCriteria * criteria to check * @return <code>true</code> if the criteria is filtering by ID */ private boolean isIDCriteria(JSONObject jsonCriteria) { if (!jsonCriteria.has("criteria")) { return false; } try { JSONArray criteria = jsonCriteria.getJSONArray("criteria"); for (int i = 0; i < criteria.length(); i++) { JSONObject criterion = criteria.getJSONObject(i); if (criterion.has("fieldName") && JsonConstants.ID.equals(criterion.getString("fieldName"))) { return true; } } } catch (JSONException e) { log.error("Error parsing criteria " + jsonCriteria, e); } return false; } }