/********************************************************************** * This file is part of iDempiere ERP Open Source * * http://www.idempiere.org * * * * Copyright (C) Contributors * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * * MA 02110-1301, USA. * * * * Contributors: * * - Trek Global Corporation * * - Heng Sin Low * **********************************************************************/ package com.trekglobal.idempiere.rest.api.v1.resource.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.xml.bind.DatatypeConverter; import org.adempiere.base.event.EventManager; import org.adempiere.base.event.EventProperty; import org.adempiere.base.event.IEventManager; import org.adempiere.exceptions.AdempiereException; import org.adempiere.exceptions.CrossTenantException; import org.compiere.model.MAttachment; import org.compiere.model.MAttachmentEntry; import org.compiere.model.MTable; import org.compiere.model.MWindow; import org.compiere.model.PO; import org.compiere.model.Query; import org.compiere.process.DocAction; import org.compiere.process.ProcessInfo; import org.compiere.util.CLogger; import org.compiere.util.DB; import org.compiere.util.Env; import org.compiere.util.MimeType; import org.compiere.util.Msg; import org.compiere.util.Trx; import org.compiere.util.Util; import org.compiere.wf.MWorkflow; import org.osgi.service.event.Event; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.trekglobal.idempiere.rest.api.json.IDempiereRestException; import com.trekglobal.idempiere.rest.api.json.IPOSerializer; import com.trekglobal.idempiere.rest.api.json.ModelHelper; import com.trekglobal.idempiere.rest.api.json.POParser; import com.trekglobal.idempiere.rest.api.json.ResponseUtils; import com.trekglobal.idempiere.rest.api.json.RestUtils; import com.trekglobal.idempiere.rest.api.json.TypeConverterUtils; import com.trekglobal.idempiere.rest.api.json.expand.ExpandParser; import com.trekglobal.idempiere.rest.api.json.expand.ExpandUtils; import com.trekglobal.idempiere.rest.api.json.filter.ConvertedQuery; import com.trekglobal.idempiere.rest.api.json.filter.IQueryConverter; import com.trekglobal.idempiere.rest.api.model.MRestView; import com.trekglobal.idempiere.rest.api.model.MRestViewRelated; import com.trekglobal.idempiere.rest.api.util.ThreadLocalTrx; import com.trekglobal.idempiere.rest.api.v1.resource.ModelResource; import com.trekglobal.idempiere.rest.api.v1.resource.WindowResource; import com.trekglobal.idempiere.rest.api.v1.resource.file.FileStreamingOutput; /** * @author hengsin * */ public class ModelResourceImpl implements ModelResource { private final static CLogger log = CLogger.getCLogger(ModelResourceImpl.class); public static final String PO_BEFORE_REST_SAVE = "idempiere-rest/po/beforeSave"; public static final String PO_AFTER_REST_SAVE = "idempiere-rest/po/afterSave"; private boolean useRestView = false; /** * default constructor */ public ModelResourceImpl() { } /** * Use Rest_View as entry point */ protected ModelResourceImpl restView() { useRestView = true; return this; } public Response getPO(String tableName, String id, String details, String select, String showsql) { return getPO(tableName, id, details, select, null, showsql, null); } @Override public Response getPO(String tableName, String id, String details, String select, String showsql, String showlabel) { return getPO(tableName, id, details, select, null, showsql, showlabel); } /** * * @param tableName table or rest view definition name * @param id id or uuid * @param details child/link entity * @param multiProperty comma separated columns * @param singleProperty single column * @param showsql * @param showlabel * @return */ private Response getPO(String tableName, String id, String details, String multiProperty, String singleProperty, String showsql, String showlabel) { try { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else throw new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND); } RestUtils.getTableAndCheckAccess(tableName, false); String[] includes = null; if (!Util.isEmpty(multiProperty, true)) { if (view != null) multiProperty = toColumnNames(view, multiProperty); includes = RestUtils.getSelectedColumns(tableName, multiProperty); } else if (!Util.isEmpty(singleProperty, true)) { if (view != null) { String original = singleProperty; singleProperty = toColumnNames(view, singleProperty); if (Util.isEmpty(singleProperty, true)) return ResponseUtils.getResponseError(Status.NOT_FOUND, "Invalid property name", "No match found for table name: ", original); } MTable table = MTable.get(Env.getCtx(), tableName); if (table.getColumnIndex(singleProperty) < 0) { return ResponseUtils.getResponseError(Status.NOT_FOUND, "Invalid property name", "No match found for column name: ", singleProperty); } includes = new String[] {singleProperty}; } Query query = RestUtils.getQuery(tableName, id, true, false); if (includes != null && includes.length > 0) query.selectColumns(includes); PO po = query.first(); POParser poParser = new POParser(tableName, id, po); if (poParser.isValidPO()) { IPOSerializer serializer = IPOSerializer.getPOSerializer(tableName, po.getClass()); JsonObject json; boolean showData = (showsql == null || !"nodata".equals(showsql)); if (showData) json = serializer.toJson(po, view, includes, null); else { json = new JsonObject(); showlabel = null; } if (showsql != null) { json.addProperty("sql-command", DB.getDatabase().convertStatement(query.getSQL())); } if (!Util.isEmpty(details, true)) { expandDetailsInJsonObject(po, view, json, json, details, showsql != null, showData, showlabel); } else if (view != null) { //add auto expand detail view definition MRestViewRelated[] relateds = view.getRelatedViews(); if (relateds != null && relateds.length > 0) { StringBuilder expands = new StringBuilder(); for (MRestViewRelated related : relateds) { if (related.isRestAutoExpand()) { if (expands.length() > 0) expands.append(","); autoExpandRelated(expands, related); } } if (expands.length() > 0) { expandDetailsInJsonObject(po, view, json, json, expands.toString(), showsql != null, showData, showlabel); } } } if (showlabel != null) ExpandUtils.addAssignedLabelsToJson(po, showlabel, json); return Response.ok(json.toString()).build(); } else { return poParser.getResponseError(); } } catch(Exception ex) { return ResponseUtils.getResponseErrorFromException(ex, "GET Error"); } } private void autoExpandRelated(StringBuilder expands, MRestViewRelated related) { expands.append(related.getName()); MRestView view = MRestView.get(related.getREST_RelatedRestView_ID()); //check tree, avoid infinite loop if (view.getREST_View_ID() == related.getREST_View_ID()) return; MRestViewRelated[] relateds = view.getRelatedViews(); if (relateds != null && relateds.length > 0) { StringBuilder childs = new StringBuilder(); for (MRestViewRelated rv : relateds) { if (rv.isRestAutoExpand()) { if (childs.length() > 0) childs.append(","); autoExpandRelated(childs, rv); } } if (childs.length() > 0) { expands.append("($expand=") .append(childs.toString()) .append(")"); } } } /** * Convert view property name to table column name (if needed) * @param view * @param propertyNames comma separated list of view property or table column name * @return converted names */ private String toColumnNames(MRestView view, String propertyNames) { String[] columns = propertyNames.split("[,]"); columns = view.toColumnNames(columns, true); return String.join(",", columns); } private void expandDetailsInJsonObject(PO po, MRestView view, JsonObject masterJsonObject, JsonObject detailJsonObject, String expandParameter, boolean showSql, boolean showData, String showlabel) { ExpandParser expandParser = new ExpandParser(po, view, expandParameter, showlabel); if (showSql) ExpandUtils.addDetailSQLCommandToJson(expandParser.getTableNameSQLStatementMap(), masterJsonObject); if (showData) ExpandUtils.addDetailDataToJson(expandParser.getTableNameChildArrayMap(), detailJsonObject); } @Override public Response getPOProperty(String tableName, String id, String propertyName, String showsql) { return getPO(tableName, id, null, null, propertyName, showsql, null); } @Override public Response getModels(String filter) { IQueryConverter converter = IQueryConverter.getQueryConverter("DEFAULT"); try { ConvertedQuery convertedStatement = converter.convertStatement(MTable.Table_Name, filter); if (log.isLoggable(Level.INFO)) log.info("Where Clause: " + convertedStatement.getWhereClause()); Query query = new Query(Env.getCtx(), MTable.Table_Name, convertedStatement.getWhereClause(), null); query.setOnlyActiveRecords(true).setApplyAccessFilter(true); query.setParameters(convertedStatement.getParameters()); List tables = query.setOrderBy("AD_Table.TableName").list(); JsonArray array = new JsonArray(); for(MTable table : tables) { if (RestUtils.hasAccess(table, false)) { JsonObject json = new JsonObject(); json.addProperty("id", table.getAD_Table_ID()); if (!Util.isEmpty(table.getAD_Table_UU())) { json.addProperty("uid", table.getAD_Table_UU()); } json.addProperty("model-name", table.getTableName().toLowerCase()); json.addProperty("name", table.getName()); if (!Util.isEmpty(table.getDescription())) { json.addProperty("description", table.getDescription()); } array.add(json); } } JsonObject json = new JsonObject(); json.add("models", array); return Response.ok(json.toString()).build(); } catch (Exception ex) { return ResponseUtils.getResponseErrorFromException(ex, "GET Error"); } } public Response getPOs(String tableName, String details, String filter, String order, String select, int top, int skip, String validationRuleID, String context, String showsql) { return getPOs(tableName, details, filter, order, select, top, skip, validationRuleID, context, showsql, null, null); } @Override public Response getPOs(String tableName, String details, String filter, String order, String select, int top, int skip, String validationRuleID, String context, String showsql, String label, String showlabel) { try { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else throw new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND); } RestUtils.getTableAndCheckAccess(tableName, false); ModelHelper modelHelper = new ModelHelper(tableName, filter, order, top, skip, validationRuleID, context, label); if (view != null && !Util.isEmpty(select, true)) { select = toColumnNames(view, select); } String[] includes = RestUtils.getSelectedColumns(tableName, select); if (view != null) { modelHelper.setView(view); } List list = modelHelper.getPOsFromRequest(includes); JsonArray array = new JsonArray(); if (list != null) { IPOSerializer serializer = IPOSerializer.getPOSerializer(tableName, MTable.getClass(tableName)); boolean showData = (showsql == null || !"nodata".equals(showsql)); if (!showData) showlabel = null; JsonObject json = new JsonObject(); json.addProperty("page-count", modelHelper.getPageCount()); json.addProperty("records-size", modelHelper.getTop()); json.addProperty("skip-records", modelHelper.getSkip()); json.addProperty("row-count", modelHelper.getRowCount()); if (showsql != null) { json.addProperty("sql-command", DB.getDatabase().convertStatement(modelHelper.getSQLStatement())); } for (PO po : list) { JsonObject detailJson = serializer.toJson(po, view, includes, null); if (!Util.isEmpty(details, true)) { expandDetailsInJsonObject(po, view, json, detailJson, details, showsql != null, showData, showlabel); } else if (view != null) { //add auto expand detail view definition MRestViewRelated[] relateds = view.getRelatedViews(); if (relateds != null && relateds.length > 0) { StringBuilder expands = new StringBuilder(); for (MRestViewRelated related : relateds) { if (related.isRestAutoExpand()) { if (expands.length() > 0) expands.append(","); autoExpandRelated(expands, related); } } if (expands.length() > 0) { expandDetailsInJsonObject(po, view, json, detailJson, expands.toString(), showsql != null, showData, showlabel); } } } if (showlabel != null) ExpandUtils.addAssignedLabelsToJson(po, showlabel, detailJson); array.add(detailJson); } if (showData) json.add("records", array); json.addProperty("array-count", array.size()); return Response.ok(json.toString()) .header("X-Page-Count", modelHelper.getPageCount()) .header("X-Records-Size", modelHelper.getTop()) .header("X-Skip-Records", modelHelper.getSkip()) .header("X-Row-Count", modelHelper.getRowCount()) .header("X-Array-Count", array.size()) .build(); } else { JsonObject json = new JsonObject(); json.add("records", array); return Response.ok(json.toString()).build(); } } catch (Exception ex) { return ResponseUtils.getResponseErrorFromException(ex, "GET Error"); } } @Override public Response create(String tableName, String jsonText) { String threadLocalTrxName = ThreadLocalTrx.getTrxName(); Trx trx = threadLocalTrxName != null ? Trx.get(threadLocalTrxName, false) : Trx.get(Trx.createTrxName(), true); try { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else throw new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND); } MTable table = RestUtils.getTableAndCheckAccess(tableName, true); if (threadLocalTrxName == null) trx.start(); Gson gson = new GsonBuilder().create(); JsonObject jsonObject = gson.fromJson(jsonText, JsonObject.class); IPOSerializer serializer = IPOSerializer.getPOSerializer(tableName, MTable.getClass(tableName)); PO po = serializer.fromJson(jsonObject, table, view); if (!RestUtils.hasRoleUpdateAccess(po.getAD_Client_ID(), po.getAD_Org_ID(), po.get_Table_ID(), 0, true)) return ResponseUtils.getResponseError(Status.FORBIDDEN, "Update error", "Role does not have access",""); po.set_TrxName(trx.getTrxName()); fireRestSaveEvent(po, PO_BEFORE_REST_SAVE, true); try { po.validForeignKeysEx(); po.saveEx(); fireRestSaveEvent(po, PO_AFTER_REST_SAVE, true); } catch (CrossTenantException e) { trx.rollback(); return ResponseUtils.getResponseError(Status.INTERNAL_SERVER_ERROR, "Save error", "Foreign ID " + e.getFKValue() + " not found in ", String.valueOf(e.getFKColumn())); } catch (Exception ex) { trx.rollback(); return ResponseUtils.getResponseErrorFromException(ex, "Save error"); } Map detailMap = new LinkedHashMap<>(); Set fields = jsonObject.keySet(); for(String field : fields) { String strError = createChild(field, jsonObject, po, view, detailMap, trx); if(strError != null) return ResponseUtils.getResponseError(Status.INTERNAL_SERVER_ERROR, "Save error", "Save error with exception: ", strError); } StringBuilder processMsg = new StringBuilder(); String processError = runDocAction(po, jsonObject, processMsg); if (!Util.isEmpty(processError, true)) { trx.rollback(); log.warning("Encounter exception during execution of document action in REST: " + processError); return ResponseUtils.getResponseError(Status.INTERNAL_SERVER_ERROR, Msg.getMsg(po.getCtx(), "FailedProcessingDocument"), processError, ""); } if (threadLocalTrxName == null) trx.commit(true); po.load(trx.getTrxName()); jsonObject = serializer.toJson(po, view); if (processMsg.length() > 0) jsonObject.addProperty("doc-processmsg", processMsg.toString()); if (detailMap.size() > 0) { for(String childTableName : detailMap.keySet()) { JsonArray childArray = detailMap.get(childTableName); jsonObject.add(childTableName, childArray); } } return Response.status(Status.CREATED).entity(jsonObject.toString()).build(); } catch (Exception ex) { trx.rollback(); return ResponseUtils.getResponseErrorFromException(ex, "Server error"); } finally { if (threadLocalTrxName == null) trx.close(); } } /** * Recursive Method to Create Children * @param field * @param jsonObject * @param po * @param view * @param detailMap * @param trx * @return */ private String createChild(String field, JsonObject jsonObject, PO po, MRestView view, Map detailMap, Trx trx) { JsonElement fieldElement = jsonObject.get(field); if (fieldElement != null && fieldElement.isJsonArray()) { String childTableName = field; MRestView childView = null; if (view != null) { //find child view definition MRestViewRelated[] relateds = view.getRelatedViews(); for(MRestViewRelated related : relateds) { MRestView relatedView = new MRestView(Env.getCtx(), related.getREST_RelatedRestView_ID(), null); String tableName = MTable.getTableName(Env.getCtx(), relatedView.getAD_Table_ID()); if (related.getName().equals(field)) { childTableName = tableName; childView = relatedView; break; } else if (tableName.equals(field)) { childView = relatedView; break; } } if (childView == null) return null; } MTable childTable = MTable.get(Env.getCtx(), childTableName); if (!RestUtils.isValidDetailTable(childTable, RestUtils.getKeyColumnName(po.get_TableName()))) { throw new IDempiereRestException("Wrong detail", "Cannot create detail records for the table because it has no column that links to the parent table: " + childTableName, Status.INTERNAL_SERVER_ERROR); } if (childTable != null && childTable.getAD_Table_ID() > 0) { IPOSerializer childSerializer = IPOSerializer.getPOSerializer(childTableName, MTable.getClass(childTableName)); JsonArray fieldArray = fieldElement.getAsJsonArray(); JsonArray savedArray = new JsonArray(); try { MRestView finalChildView = childView; fieldArray.forEach(e -> { if (e.isJsonObject()) { JsonObject childJsonObject = e.getAsJsonObject(); PO childPO = childSerializer.fromJson(childJsonObject, childTable, finalChildView); if (!RestUtils.hasRoleUpdateAccess(childPO.getAD_Client_ID(), childPO.getAD_Org_ID(), childPO.get_Table_ID(), 0, true)) throw new AdempiereException("AccessCannotUpdate"); childPO.set_TrxName(trx.getTrxName()); childPO.set_ValueOfColumn(RestUtils.getKeyColumnName(po.get_TableName()), po.get_ID()); fireRestSaveEvent(childPO, PO_BEFORE_REST_SAVE, true); childPO.validForeignKeysEx(); childPO.saveEx(); fireRestSaveEvent(childPO, PO_AFTER_REST_SAVE, true); childJsonObject = childSerializer.toJson(childPO, finalChildView); JsonObject newChildJsonObject = e.getAsJsonObject(); Map childDetailMap = new LinkedHashMap<>(); Set fields = newChildJsonObject.keySet(); for(String childField : fields) { String strError = createChild(childField, newChildJsonObject, childPO, finalChildView, childDetailMap, trx); if(strError != null) throw new AdempiereException(strError); } if (childDetailMap.size() > 0) { for(String tableName : childDetailMap.keySet()) { JsonArray childArray = childDetailMap.get(tableName); childJsonObject.add(tableName, childArray); } } savedArray.add(childJsonObject); StringBuilder processMsg = new StringBuilder(); String processError = runDocAction(childPO, newChildJsonObject, processMsg); if(processError != null) throw new AdempiereException(processError + " - " + processMsg != null ? processMsg.toString() : ""); } }); if (savedArray.size() > 0) detailMap.put(field, savedArray); } catch (Exception ex) { trx.rollback(); log.log(Level.SEVERE, ex.getMessage(), ex); if (ex instanceof CrossTenantException) return "Foreign ID " + ((CrossTenantException)ex).getFKValue() + " not found in " + String.valueOf(((CrossTenantException)ex).getFKColumn()); return ex.getMessage(); } } } return null; } @Override public Response update(String name, String id, String jsonText) { MRestView view = null; if (useRestView) { view = RestUtils.getView(name); if (view != null) name = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + name, Status.NOT_FOUND), "Not found"); } String tableName = name; POParser poParser = new POParser(tableName, id, true, true, ThreadLocalTrx.getTrxName()); if (!poParser.isValidPO()) { return poParser.getResponseError(); } PO po = poParser.getPO(); if (!RestUtils.hasRoleUpdateAccess(po.getAD_Client_ID(), po.getAD_Org_ID(), po.get_Table_ID(), po.get_ID(), false)) return ResponseUtils.getResponseError(Status.FORBIDDEN, "Update error", "Role does not have access",""); String threadLocalTrxName = ThreadLocalTrx.getTrxName(); Trx trx = threadLocalTrxName != null ? Trx.get(threadLocalTrxName, false) : Trx.get(Trx.createTrxName(), true); try { if (threadLocalTrxName == null) trx.start(); Gson gson = new GsonBuilder().create(); JsonObject jsonObject = gson.fromJson(jsonText, JsonObject.class); IPOSerializer serializer = IPOSerializer.getPOSerializer(tableName, MTable.getClass(tableName)); po = serializer.fromJson(jsonObject, po, view); po.set_TrxName(trx.getTrxName()); fireRestSaveEvent(po, PO_BEFORE_REST_SAVE, false); try { po.validForeignKeysEx(); po.saveEx(); fireRestSaveEvent(po, PO_AFTER_REST_SAVE, false); } catch (CrossTenantException e) { trx.rollback(); return ResponseUtils.getResponseError(Status.INTERNAL_SERVER_ERROR, "Save error", "Foreign ID " + e.getFKValue() + " not found in ", String.valueOf(e.getFKColumn())); } catch (Exception ex) { trx.rollback(); return ResponseUtils.getResponseErrorFromException(ex, "Save error"); } Map detailMap = new LinkedHashMap<>(); Set fields = jsonObject.keySet(); final int parentId = po.get_ID(); for(String field : fields) { JsonElement fieldElement = jsonObject.get(field); if (fieldElement != null && fieldElement.isJsonArray()) { MRestView childView = null; if (view != null) { //find child view definition MRestViewRelated[] relateds = view.getRelatedViews(); for(MRestViewRelated related : relateds) { MRestView relatedView = new MRestView(Env.getCtx(), related.getREST_RelatedRestView_ID(), null); String tName = MTable.getTableName(Env.getCtx(), relatedView.getAD_Table_ID()); if (related.getName().equals(field)) { childView = relatedView; break; } else if (tName.equals(field)) { childView = relatedView; break; } } if (childView == null) continue; } String childTableName = childView != null ? MTable.getTableName(Env.getCtx(), childView.getAD_Table_ID()) : field; MTable childTable = MTable.get(Env.getCtx(), childTableName); if (!RestUtils.isValidDetailTable(childTable, RestUtils.getKeyColumnName(po.get_TableName()))) { throw new IDempiereRestException("Wrong detail", "Cannot create/update detail records for the table because it has no column that links to the parent table: " + childTableName, Status.INTERNAL_SERVER_ERROR); } if (childTable != null && childTable.getAD_Table_ID() > 0) { IPOSerializer childSerializer = IPOSerializer.getPOSerializer(childTableName, MTable.getClass(childTableName)); JsonArray fieldArray = fieldElement.getAsJsonArray(); JsonArray savedArray = new JsonArray(); MRestView finalChildView = childView; try { fieldArray.forEach(e -> { if (e.isJsonObject()) { JsonObject childJsonObject = e.getAsJsonObject(); PO childPO = loadPO(childTableName, childJsonObject); boolean delete = childJsonObject.get("(delete)") != null && childJsonObject.get("(delete)").getAsBoolean(); if (delete && childPO == null) throw new IDempiereRestException("Delete Error", "Cannot delete non-existing record", Status.NOT_FOUND); if (childPO == null) { childPO = childSerializer.fromJson(childJsonObject, childTable, finalChildView); childPO.set_ValueOfColumn(RestUtils.getKeyColumnName(tableName), parentId); } else if (!delete){ childPO = childSerializer.fromJson(childJsonObject, childPO, finalChildView); } childPO.set_TrxName(trx.getTrxName()); if (delete) { childPO.deleteEx(true); } else { fireRestSaveEvent(childPO, PO_BEFORE_REST_SAVE, false); childPO.validForeignKeysEx(); childPO.saveEx(); fireRestSaveEvent(childPO, PO_AFTER_REST_SAVE, false); childJsonObject = serializer.toJson(childPO, finalChildView); savedArray.add(childJsonObject); } } }); if (savedArray.size() > 0) detailMap.put(field, savedArray); } catch (Exception ex) { trx.rollback(); if (ex instanceof CrossTenantException) return ResponseUtils.getResponseError(Status.INTERNAL_SERVER_ERROR, "Save Error", "Foreign ID " + ((CrossTenantException)ex).getFKValue() + " not found in ", String.valueOf(((CrossTenantException)ex).getFKColumn())); return ResponseUtils.getResponseErrorFromException(ex, "Save error"); } } } } StringBuilder processMsg = new StringBuilder(); String error = runDocAction(po, jsonObject, processMsg); if (Util.isEmpty(error, true)) { if (threadLocalTrxName == null) trx.commit(true); } else { trx.rollback(); log.warning("Encounter exception during execution of document action in REST: " + error); return ResponseUtils.getResponseError(Status.INTERNAL_SERVER_ERROR, Msg.getMsg(po.getCtx(), "FailedProcessingDocument"), error, ""); } po.load(trx.getTrxName()); jsonObject = serializer.toJson(po, view); if (processMsg.length() > 0) jsonObject.addProperty("doc-processmsg", processMsg.toString()); if (detailMap.size() > 0) { for(String field : detailMap.keySet()) { JsonArray child = detailMap.get(field); jsonObject.add(field, child); } } return Response.status(Status.OK).entity(jsonObject.toString()).build(); } catch (Exception ex) { trx.rollback(); return ResponseUtils.getResponseErrorFromException(ex, "Update error"); } finally { if (threadLocalTrxName == null) trx.close(); } } /** * Fire the PO_BEFORE_REST_SAVE/PO_AFTER_REST_SAVE event, to catch and manipulate the object before the model beforeSave/afterSave * @param po */ private void fireRestSaveEvent(PO po, String topic, boolean isNew) { Event event = EventManager.newEvent(topic, new EventProperty(EventManager.EVENT_DATA, po), new EventProperty("tableName", po.get_TableName()), new EventProperty("isNew", isNew)); EventManager.getInstance().sendEvent(event); @SuppressWarnings("unchecked") List errors = (List) event.getProperty(IEventManager.EVENT_ERROR_MESSAGES); if (errors != null && !errors.isEmpty()) throw new AdempiereException(errors.get(0)); } @Override public Response delete(String tableName, String id) { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND), "Not found"); } POParser poParser = new POParser(tableName, id, true, true, ThreadLocalTrx.getTrxName()); if (poParser.isValidPO()) { PO po = poParser.getPO(); if (!RestUtils.hasRoleUpdateAccess(po.getAD_Client_ID(), po.getAD_Org_ID(), po.get_Table_ID(), 0, true)) { return ResponseUtils.getResponseError(Status.FORBIDDEN, "Delete error", "AccessCannotDelete",""); } try { po.deleteEx(true); JsonObject json = new JsonObject(); json.addProperty("msg", Msg.getMsg(Env.getCtx(), "Deleted")); return Response.ok(json.toString()).build(); } catch (Exception ex) { String threadLocalTrxName = ThreadLocalTrx.getTrxName(); if (threadLocalTrxName != null) { Trx trx = Trx.get(threadLocalTrxName, false); if (trx != null && trx.isActive()) trx.rollback(); } return ResponseUtils.getResponseErrorFromException(ex, "Delete error"); } } else { return poParser.getResponseError(); } } @Override public Response getAttachments(String tableName, String id) { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND), "Not found"); } JsonArray array = new JsonArray(); POParser poParser = new POParser(tableName, id, true, false); if (poParser.isValidPO()) { PO po = poParser.getPO(); MAttachment attachment = po.getAttachment(); if (attachment != null) { for(MAttachmentEntry entry : attachment.getEntries()) { JsonObject entryJsonObject = new JsonObject(); entryJsonObject.addProperty("name", entry.getName()); if (!Util.isEmpty(entry.getContentType(), true)) entryJsonObject.addProperty("contentType", entry.getContentType()); array.add(entryJsonObject); } } JsonObject json = new JsonObject(); json.add("attachments", array); return Response.ok(json.toString()).build(); } else { return poParser.getResponseError(); } } @Override public Response getAttachmentsAsZip(String tableName, String id, String asJson) { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND), "Not found"); } POParser poParser = new POParser(tableName, id, true, false); if (poParser.isValidPO()) { PO po = poParser.getPO(); MAttachment attachment = po.getAttachment(); if (attachment != null) { try { File zipFile = attachment.saveAsZip(); if (zipFile != null) { if (asJson == null) { FileStreamingOutput fso = new FileStreamingOutput(zipFile); return Response.ok(fso).build(); } else { JsonObject json = new JsonObject(); byte[] binaryData = Files.readAllBytes(zipFile.toPath()); String data = Base64.getEncoder().encodeToString(binaryData); json.addProperty("data", data); return Response.ok(json.toString()).build(); } } } catch (IOException ex) { return ResponseUtils.getResponseErrorFromException(ex, "IO error"); } } return Response.status(Status.NO_CONTENT).build(); } else { return poParser.getResponseError(); } } @Override public Response createAttachmentsFromZip(String tableName, String id, String jsonText) { Gson gson = new GsonBuilder().create(); JsonObject jsonObject = gson.fromJson(jsonText, JsonObject.class); boolean overwrite = false; JsonElement jsonElement = jsonObject.get("overwrite"); if (jsonElement != null && jsonElement.isJsonPrimitive()) overwrite = jsonElement.getAsBoolean(); jsonElement = jsonObject.get("data"); if (jsonElement == null || !jsonElement.isJsonPrimitive()) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "data property is mandatory", "", ""); String base64Content = jsonElement.getAsString(); if (Util.isEmpty(base64Content, true)) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "data property is mandatory", "", ""); MRestView view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); POParser poParser = new POParser(tableName, id, true, false, ThreadLocalTrx.getTrxName()); if (poParser.isValidPO()) { PO po = poParser.getPO(); byte[] data = DatatypeConverter.parseBase64Binary(base64Content); if (data == null || data.length == 0) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "Can't parse data", "Can't parse data in Json content, not base64 encoded", ""); MAttachment attachment = po.getAttachment(); if (attachment == null) attachment = po.createAttachment(); try (ZipInputStream stream = new ZipInputStream(new ByteArrayInputStream(data))) { ZipEntry entry; while ((entry = stream.getNextEntry()) != null) { String name = entry.getName(); for(int i = 0; i < attachment.getEntryCount(); i++) { MAttachmentEntry e = attachment.getEntry(i); if (e.getName().equals(name)) { if (overwrite) { attachment.deleteEntry(i); break; } else { return ResponseUtils.getResponseError(Status.CONFLICT, "Duplicate file name", "Duplicate file name: ", name); } } } byte[] buffer = new byte[2048]; ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len; while ((len = stream.read(buffer)) > 0) { bos.write(buffer, 0, len); } attachment.addEntry(name, bos.toByteArray()); } attachment.saveEx(); } catch (Exception ex) { String threadLocalTrxName = ThreadLocalTrx.getTrxName(); if (threadLocalTrxName != null) { Trx trx = Trx.get(threadLocalTrxName, false); if (trx != null && trx.isActive()) trx.rollback(); } return ResponseUtils.getResponseErrorFromException(ex, "Create attachment error"); } return Response.status(Status.CREATED).build(); } else { return poParser.getResponseError(); } } @Override public Response getAttachmentEntry(String tableName, String id, String fileName, String asJson) { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND), "Not found"); } POParser poParser = new POParser(tableName, id, true, false); if (poParser.isValidPO()) { PO po = poParser.getPO(); MAttachment attachment = po.getAttachment(); if (attachment != null) { for(MAttachmentEntry entry : attachment.getEntries()) { if (entry.getName().equals(fileName)) { try { Path tempPath = Files.createTempDirectory(tableName); File tempFolder = tempPath.toFile(); File zipFile = new File(tempFolder, fileName); zipFile = entry.getFile(zipFile); if (asJson == null) { FileStreamingOutput fso = new FileStreamingOutput(zipFile); String contentType = MimeType.getMimeType(zipFile.getName()); return Response.ok(fso).header("Content-Type", contentType).build(); } else { JsonObject json = new JsonObject(); byte[] binaryData = Files.readAllBytes(zipFile.toPath()); String data = Base64.getEncoder().encodeToString(binaryData); json.addProperty("data", data); return Response.ok(json.toString()).build(); } } catch (IOException ex) { return ResponseUtils.getResponseErrorFromException(ex, "IO error"); } } } } return Response.status(Status.NO_CONTENT).build(); } else { return poParser.getResponseError(); } } @Override public Response addAttachmentEntry(String tableName, String id, String jsonText) { Gson gson = new GsonBuilder().create(); JsonObject jsonObject = gson.fromJson(jsonText, JsonObject.class); JsonElement jsonElement = jsonObject.get("name"); if (jsonElement == null || !jsonElement.isJsonPrimitive()) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "name property is mandatory", "", ""); String fileName = jsonElement.getAsString(); if (Util.isEmpty(fileName, true)) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "name property is mandatory", "", ""); jsonElement = jsonObject.get("data"); if (jsonElement == null || !jsonElement.isJsonPrimitive()) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "data property is mandatory", "", ""); String base64Content = jsonElement.getAsString(); if (Util.isEmpty(base64Content, true)) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "data property is mandatory", "", ""); boolean overwrite = false; jsonElement = jsonObject.get("overwrite"); if (jsonElement != null && jsonElement.isJsonPrimitive()) overwrite = jsonElement.getAsBoolean(); MRestView view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); POParser poParser = new POParser(tableName, id, true, false, ThreadLocalTrx.getTrxName()); if (poParser.isValidPO()) { PO po = poParser.getPO(); byte[] data = DatatypeConverter.parseBase64Binary(base64Content); if (data == null || data.length == 0) return ResponseUtils.getResponseError(Status.BAD_REQUEST, "Can't parse data", "Can't parse data in Json content, not base64 encoded", ""); MAttachment attachment = po.getAttachment(); if (attachment == null) attachment = po.createAttachment(); for(int i = 0; i < attachment.getEntryCount(); i++) { MAttachmentEntry entry = attachment.getEntry(i); if (entry.getName().equals(fileName)) { if (overwrite) { attachment.deleteEntry(i); break; } else { return ResponseUtils.getResponseError(Status.CONFLICT, "Duplicate file name", "Duplicate file name: ", fileName); } } } try { attachment.addEntry(fileName, data); attachment.saveEx(); } catch (Exception ex) { String threadLocalTrxName = ThreadLocalTrx.getTrxName(); if (threadLocalTrxName != null) { Trx trx = Trx.get(threadLocalTrxName, false); if (trx != null && trx.isActive()) trx.rollback(); } return ResponseUtils.getResponseErrorFromException(ex, "Save error"); } return Response.status(Status.CREATED).build(); } else { return poParser.getResponseError(); } } @Override public Response deleteAttachments(String tableName, String id) { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND), "Not found"); } POParser poParser = new POParser(tableName, id, true, false, ThreadLocalTrx.getTrxName()); if (poParser.isValidPO()) { PO po = poParser.getPO(); MAttachment attachment = po.getAttachment(); if (attachment != null) { try { attachment.deleteEx(true); } catch (Exception ex) { String threadLocalTrxName = ThreadLocalTrx.getTrxName(); if (threadLocalTrxName != null) { Trx trx = Trx.get(threadLocalTrxName, false); if (trx != null && trx.isActive()) trx.rollback(); } return ResponseUtils.getResponseErrorFromException(ex, "Delete error"); } JsonObject json = new JsonObject(); json.addProperty("msg", Msg.getMsg(Env.getCtx(), "Deleted")); return Response.ok(json.toString()).build(); } else { return ResponseUtils.getResponseError(Status.NOT_FOUND, "No attachments", "No attachment is found for record with id ", id); } } else { return poParser.getResponseError(); } } @Override public Response deleteAttachmentEntry(String tableName, String id, String fileName) { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND), "Not found"); } POParser poParser = new POParser(tableName, id, true, false, ThreadLocalTrx.getTrxName()); if (poParser.isValidPO()) { PO po = poParser.getPO(); MAttachment attachment = po.getAttachment(); if (attachment != null) { int i = 0; for(MAttachmentEntry entry : attachment.getEntries()) { if (entry.getName().equals(fileName)) { if (attachment.deleteEntry(i)) { try { attachment.saveEx(); } catch (Exception ex) { String threadLocalTrxName = ThreadLocalTrx.getTrxName(); if (threadLocalTrxName != null) { Trx trx = Trx.get(threadLocalTrxName, false); if (trx != null && trx.isActive()) trx.rollback(); } return ResponseUtils.getResponseErrorFromException(ex, "Delete error"); } JsonObject json = new JsonObject(); json.addProperty("msg", Msg.getMsg(Env.getCtx(), "Deleted")); return Response.ok(json.toString()).build(); } else { return ResponseUtils.getResponseError(Status.INTERNAL_SERVER_ERROR, "Fail to remove attachment entry", "", ""); } } i++; } return ResponseUtils.getResponseError(Status.NOT_FOUND, "No matching attachment entry", "No attachment entry is found for name ", fileName); } else { return ResponseUtils.getResponseError(Status.NOT_FOUND, "No attachments", "No attachment is found for record with id ", id); } } else { return poParser.getResponseError(); } } @Override public Response printModelRecord(String tableName, String id, String reportType) { MRestView view = null; if (useRestView) { view = RestUtils.getView(tableName); if (view != null) tableName = MTable.getTableName(Env.getCtx(), view.getAD_Table_ID()); else return ResponseUtils.getResponseErrorFromException(new IDempiereRestException("Invalid rest view name", "No match found for rest view name: " + tableName, Status.NOT_FOUND), "Not found"); } POParser poParser = new POParser(tableName, id, true, false); if (poParser.isValidPO()) { PO po = poParser.getPO(); try { MTable table = RestUtils.getTableAndCheckAccess(tableName, false); int windowId = Env.getZoomWindowID(table.get_ID(), po.get_ID()); if (windowId == 0) return ResponseUtils.getResponseError(Status.NOT_FOUND, "Window not found", "No valid window found for table name: ", tableName); MWindow window = MWindow.get(Env.getCtx(), windowId); String windowSlug = TypeConverterUtils.slugify(window.getName()); WindowResource windowResource = new WindowResourceImpl(); return windowResource.printWindowRecord(windowSlug, po.get_ID(), reportType); } catch (Exception ex) { return ResponseUtils.getResponseErrorFromException(ex, "Print model error"); } } else { return poParser.getResponseError(); } } private PO loadPO(String tableName, JsonObject jsonObject) { PO po = null; String idColumn = RestUtils.getKeyColumnName(tableName); String uidColumn = PO.getUUIDColumnName(tableName); JsonElement idElement = jsonObject.get("id"); if (idElement != null && idElement.isJsonPrimitive()) { Query query = new Query(Env.getCtx(), tableName, idColumn + "=?", null); query.setApplyAccessFilter(true, false); po = query.setParameters(idElement.getAsInt()).first(); } else { JsonElement uidElement = jsonObject.get("uid"); if (uidElement != null && uidElement.isJsonPrimitive()) { Query query = new Query(Env.getCtx(), tableName, uidColumn + "=?", null); query.setApplyAccessFilter(true, false); po = query.setParameters(uidElement.getAsString()).first(); } } return po; } private String runDocAction(PO po, JsonObject jsonObject, StringBuilder processMsg) { if (po instanceof DocAction) { JsonElement docActionElement = jsonObject.get("doc-action"); if (docActionElement != null) { String docAction = null; if (docActionElement.isJsonPrimitive()) { docAction = docActionElement.getAsString(); } else if (docActionElement.isJsonObject()) { JsonObject docActionJsonObject = docActionElement.getAsJsonObject(); docActionElement = docActionJsonObject.get("id"); if (docActionElement != null && docActionElement.isJsonPrimitive()) { docAction = docActionElement.getAsString(); } } if (!Util.isEmpty(docAction, true) && !DocAction.ACTION_None.equals(docAction)) { ProcessInfo processInfo = MWorkflow.runDocumentActionWorkflow(po, docAction); if (processInfo.isError()) { return Msg.parseTranslation(po.getCtx(), processInfo.getSummary()); } else { try { po.saveEx(); } catch (Exception ex) { log.log(Level.SEVERE, ex.getMessage(), ex); return ex.getMessage(); } } String pMsg = Msg.parseTranslation(po.getCtx(), ((DocAction)po).getProcessMsg()); processMsg.append(pMsg); } } } return null; } @Override public Response getModelYAML(String tableName) { MTable table = RestUtils.getTableAndCheckAccess(tableName, false); tableName = table.getTableName(); StringBuilder header = new StringBuilder(); header.append("openapi: 3.0.0\n"); header.append("info:\n"); header.append(" ".repeat(2)).append("title: models/").append(tableName).append("\n"); header.append(" ".repeat(2)).append("version: 1.0.0\n"); YAMLSchema.addServers(header); header.append("components:\n"); YAMLSchema.addSecuritySchema(header); YAMLSchema.addPredefinedParameters(header); header.append(" ".repeat(2)).append("schemas:\n"); StringBuilder body = new StringBuilder(); buildYAMLForTable(table, body); YAMLSchema.addErrorResponseReference(header); if (body.indexOf("#/components/schemas/Image") > 0) { YAMLSchema.addImageReference(header); } if (body.indexOf("#/components/schemas/Location") > 0) { YAMLSchema.addLocationReference(header, 4); } YAMLSchema.addSecurityHeader(body); body.append("paths:\n"); YAMLSchema.addAuthRequest(body); YAMLSchema.addModelRequest(tableName, false, body); return Response.status(Status.OK).entity(header.append(body.toString()).toString()).build(); } private void buildYAMLForTable(MTable table, StringBuilder builder) { builder.append(" ".repeat(4)).append(table.getTableName()).append(":\n"); builder.append(" ".repeat(6)).append("type: object\n"); builder.append(" ".repeat(6)).append("properties:\n"); builder.append(" ".repeat(8)).append("id:\n"); builder.append(" ".repeat(10)).append("type: integer\n"); builder.append(" ".repeat(10)).append("readOnly: true\n"); builder.append(" ".repeat(10)).append("description: record id\n"); builder.append(" ".repeat(8)).append("uid:\n"); builder.append(" ".repeat(10)).append("type: string\n"); builder.append(" ".repeat(10)).append("readOnly: true\n"); builder.append(" ".repeat(10)).append("description: record uuid\n"); YAMLSchema.addTableProperties(table, builder, 8); } }