All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.simplity.tp.OutputData Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2016 simplity.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.simplity.tp;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.simplity.json.JSONWriter;
import org.simplity.kernel.ApplicationError;
import org.simplity.kernel.FormattedMessage;
import org.simplity.kernel.MessageType;
import org.simplity.kernel.Tracer;
import org.simplity.kernel.comp.ValidationContext;
import org.simplity.kernel.data.DataSheet;
import org.simplity.kernel.dm.Field;
import org.simplity.kernel.util.JsonUtil;
import org.simplity.kernel.util.TextUtil;
import org.simplity.kernel.value.Value;
import org.simplity.service.ResponseWriter;
import org.simplity.service.ServiceContext;
import org.simplity.service.ServiceData;
import org.simplity.service.ServiceProtocol;

/**
 * Component that specifies what inputs are expected
 *
 * @author simplity.org
 *
 */

public class OutputData {

	static final String EMPTY_RESPONSE = "{\"" + ServiceProtocol.REQUEST_STATUS + "\":\"" + ServiceProtocol.STATUS_OK
			+ "\"}";
	/**
	 * no need to extract data for response. This field has the response text
	 * ready
	 */
	String responseTextFieldName;
	boolean justOutputEveryThing;
	/**
	 * get response from the writer in service context
	 */
	boolean outputFromWriter;
	/**
	 * comma separated list of fields to be output.
	 */
	String[] fieldNames;

	/**
	 * comma separated list of arrays. Values for arrays are in data sheet with
	 * a single column
	 */
	String[] arrayNames;
	/**
	 * sheets/fields to be output based on record definitions
	 */
	OutputRecord[] outputRecords;

	/**
	 * comma separated data sheets to be output
	 */
	String[] dataSheets;

	/**
	 * if this service wants to set/reset some session fields. Note that this
	 * directive is independent of fieldNames or outputRecords. That is if a is
	 * set as sessionFields, it is not sent to client, unless "a" is also
	 * specified as fieldNames
	 */
	String[] sessionFields;

	/**
	 * comma separated list of field names that carry key to attachments. these
	 * are processed as per attachmentManagement, and revised key is replaced as
	 * the field-value
	 */
	String[] attachmentFields;
	/**
	 * comma separated list of column names in the form
	 * sheetName.columnName,sheetName1.columnName2....
	 */
	String[] attachmentColumns;

	/**
	 * set response and session parameters
	 *
	 * @param ctx
	 * @param outData
	 */
	public void setResponse(ServiceContext ctx, ServiceData outData) {
		if (this.outputFromWriter) {
			Tracer.trace("Picking up response from writer");
			ResponseWriter writer = ctx.getWriter();
			writer.key("junk").value(100);
			writer.key(ServiceProtocol.REQUEST_STATUS).value(ServiceProtocol.STATUS_OK);
			writer.end();
			outData.setPayLoad(writer.getResponse());
			return;
		}
		if (this.responseTextFieldName != null) {
			/*
			 * service is supposed to have kept response ready for us
			 */
			Object obj = ctx.getObject(this.responseTextFieldName);
			if (obj == null) {
				obj = ctx.getValue(this.responseTextFieldName);
			}
			if (obj == null) {
				Tracer.trace("We expected a ready response in service context with name " + this.responseTextFieldName
						+ " . We are sorry that we could not locate it, and we are sending an empty response.");
				outData.setPayLoad(EMPTY_RESPONSE);
			} else {
				outData.setPayLoad(obj.toString());
			}
			return;
		}

		/*
		 * extract attachments if required
		 */
		if (this.attachmentFields != null) {
			InputData.storeFieldAttaches(this.attachmentFields, ctx, false);
		}

		if (this.attachmentColumns != null) {
			InputData.storeColumnAttaches(this.attachmentColumns, ctx, false);
		}
		/**
		 * session data if any
		 */
		if (this.sessionFields != null) {
			for (String f : this.sessionFields) {
				Object val = ctx.getValue(f);
				if (val == null) {
					val = ctx.getObject(f);
				}
				/*
				 * we may set null to remove existing values
				 */
				outData.put(f, val);
			}
		}
//		this.prepareOutData(outData, ctx);

		/*
		 * response
		 */
		JSONWriter writer = new JSONWriter();
		writer.object();
		if (this.justOutputEveryThing) {
			this.allDataToJson(writer, ctx);
		} else {
			this.dataToJson(writer, ctx);
		}
		/*
		 * we also push non-error messages
		 */
		writer.key(ServiceProtocol.MESSAGES).array();
		for (FormattedMessage msg : ctx.getMessages()) {
			if (msg.messageType != MessageType.ERROR) {
				msg.writeJsonValue(writer);
			}
		}
		writer.endArray();

		writer.endObject();
		outData.setPayLoad(writer.toString());
	}

	private void prepareOutData(ServiceData outData, ServiceContext ctx) {
		if (this.justOutputEveryThing) {
			this.copyAllToOutData(outData, ctx);
			return;
		}
		if (this.fieldNames != null) {
			for (String fieldName : this.fieldNames) {
				outData.put(fieldName, ctx.getValue(fieldName));
			}
		}

		if (this.dataSheets != null) {
			for (String sheetName : this.dataSheets) {
				DataSheet sheet = ctx.getDataSheet(sheetName);
				if (sheet == null) {
					Tracer.trace("Service context has no sheet with name " + sheetName + " for output.");
				} else {
					outData.put(sheetName, ctx.getDataSheet(sheetName));
				}
			}
		}
		if (this.outputRecords != null) {
			for (OutputRecord rec : this.outputRecords) {
				outData.put(rec.sheetName, ctx.getDataSheet(rec.sheetName));
			}
		}
		if (this.arrayNames != null) {
			for (String arrayName : this.arrayNames) {
				DataSheet sheet = ctx.getDataSheet(arrayName);
				if (sheet == null) {
					Value value = ctx.getValue(arrayName);
					if (value == null) {
						Tracer.trace("Service context has no sheet with name " + arrayName + " for output.");
						continue;
					}
					outData.put(arrayName, ctx.getValue(arrayName));
				} else {
					outData.put(arrayName, sheet);
				}
			}
		}
	}

	private void copyAllToOutData(ServiceData outData, ServiceContext ctx) {
		for (Map.Entry entry : ctx.getAllFields()) {
			outData.put(entry.getKey(), entry.getValue());
		}

		for (Map.Entry entry : ctx.getAllSheets()) {
			outData.put(entry.getKey(), entry.getValue());
		}
	}

	/**
	 * write data to the json writer based on this spec, and data available in
	 * the context
	 *
	 * @param writer
	 *            should be ready to receive key-value pairs. That is, writer
	 *            should have issued a .object()
	 * @param ctx
	 */
	public void dataToJson(JSONWriter writer, ServiceContext ctx) {
		if (this.fieldNames != null) {
			JsonUtil.addAttributes(writer, this.fieldNames, ctx);
		}

		if (this.dataSheets != null) {
			for (String sheetName : this.dataSheets) {
				sheetName = TextUtil.getFieldValue(ctx, sheetName).toString();
				DataSheet sheet = ctx.getDataSheet(sheetName);
				if (sheet == null) {
					Tracer.trace("Service context has no sheet with name " + sheetName + " for output.");
				} else {
					writer.key(sheetName);
					JsonUtil.sheetToJson(writer, sheet, null, false);
				}
			}
		}
		if (this.outputRecords != null) {
			for (OutputRecord rec : this.outputRecords) {
				rec.toJson(writer, ctx);
			}
		}
		if (this.arrayNames != null) {
			for (String arrayName : this.arrayNames) {
				DataSheet sheet = ctx.getDataSheet(arrayName);
				if (sheet == null) {
					Value value = ctx.getValue(arrayName);
					if (value == null) {
						Tracer.trace("Service context has no sheet with name " + arrayName + " for output.");
						continue;
					}
					writer.key(arrayName).array().value(value).endArray();
				} else {
					writer.key(arrayName);
					JsonUtil.sheetToArray(writer, sheet);
				}
			}
		}
	}

	/**
	 * @param writer
	 * @param ctx
	 */
	private void allDataToJson(JSONWriter writer, ServiceContext ctx) {
		for (Map.Entry entry : ctx.getAllFields()) {
			writer.key(entry.getKey()).value(entry.getValue());
		}

		for (Map.Entry entry : ctx.getAllSheets()) {
			writer.key(entry.getKey());
			DataSheet sheet = entry.getValue();
			JsonUtil.sheetToJson(writer, sheet, null, false);
		}
	}

	void onServiceStart(ServiceContext ctx) {
		if (this.outputFromWriter) {
			Tracer.trace("Started Writer for this service");
			ResponseWriter writer = new JSONWriter();
			writer.init();
			ctx.setWriter(writer);
		}
	}

	/**
	 * output all fields, and sheets, except session fields
	 *
	 * @param ctx
	 * @param response
	 * @param inData
	 */
	protected void setPayload(ServiceContext ctx, ServiceData response, ServiceData inData) {
		if (this.outputFromWriter) {
			Tracer.trace("Picking up response from writer");
			ResponseWriter writer = ctx.getWriter();
			writer.key(ServiceProtocol.REQUEST_STATUS).value(ServiceProtocol.STATUS_OK);
			writer.end();
			response.setPayLoad(writer.getResponse());
			return;
		}

		JSONWriter writer = new JSONWriter();
		writer.object();

		for (Map.Entry entry : ctx.getAllFields()) {
			String fieldName = entry.getKey();
			/*
			 * write this, but only if it didn't come as session field
			 */
			if (inData.get(fieldName) == null) {
				writer.key(fieldName);
				writer.value(entry.getValue().toObject());
			}
		}
		for (Map.Entry entry : ctx.getAllSheets()) {
			writer.key(entry.getKey());
			JsonUtil.sheetToJson(writer, entry.getValue(), null, false);
		}
		writer.endObject();
		response.setPayLoad(writer.toString());
	}

	/**
	 * get ready for a long-haul service :-)
	 */
	public void getReady() {
		if (this.outputRecords == null) {
			return;
		}

		int nbrChildren = 0;
		if (this.outputRecords != null) {
			for (OutputRecord rec : this.outputRecords) {
				rec.getReady(this);
				if (rec.parentSheetName != null) {
					nbrChildren++;
				}
			}
		}

		if (nbrChildren == 0) {
			return;
		}
		/*
		 * OK. we have to deal with hierarchical data output
		 */
		/*
		 * store child records for a parent. we know the upper limit on the
		 * array. Hence we use an array instead of list. We take care of null
		 * entries while handling it
		 */
		OutputRecord[] children = new OutputRecord[nbrChildren];

		/*
		 * we go by parent, and accumulate children
		 */
		for (OutputRecord parent : this.outputRecords) {
			String sheetName = parent.sheetName;
			if (sheetName == null) {
				continue;
			}
			/*
			 * look for possible child claiming this sheet be parent :-)
			 */
			int idx = 0;
			for (OutputRecord child : this.outputRecords) {
				if (sheetName.equals(child.parentSheetName)) {
					children[idx++] = child;
					nbrChildren--; // for tracking
				}
			}
			if (idx != 0) {
				parent.setChildren(children, idx);
				/*
				 * are we done?
				 */
				if (nbrChildren == 0) {
					break;
				}
				/*
				 * we utilized current array. Create new one for next parent.
				 */
				children = new OutputRecord[nbrChildren];
			}
		}
		/*
		 * are there children still looking for parents?
		 */
		if (nbrChildren != 0) {
			throw new ApplicationError("Please check parentSheetName attribute for inputRecords. We found "
					+ nbrChildren + " sheets with missing parent sheets!!");
		}
	}

	/**
	 * validate this specification
	 *
	 * @param ctx
	 * @return number of errors added
	 */
	int validate(ValidationContext ctx) {
		int count = 0;
		/*
		 * duplicate field names
		 */
		if (this.fieldNames != null && this.fieldNames.length > 0) {
			Set keys = new HashSet();
			for (String key : this.fieldNames) {
				if (keys.add(key) == false) {
					ctx.addError(key + " is a duplicate field name for output.");
					count++;
				}
			}
		}
		/*
		 * duplicate data sheets?
		 */
		if (this.dataSheets != null && this.dataSheets.length > 0) {
			Set keys = new HashSet();
			for (String key : this.dataSheets) {
				if (keys.add(key) == false) {
					ctx.addError(key + " is a duplicate data sheet name for output.");
					count++;
				}
			}
		}

		if (this.outputRecords == null) {
			return 0;
		}

		/*
		 * validate output records, and also keep sheet-record mapping for other
		 * validations.
		 */
		Map allSheets = new HashMap();
		int nbrParents = 0;
		for (OutputRecord rec : this.outputRecords) {
			count += rec.validate(ctx);
			allSheets.put(rec.sheetName, rec);
			if (rec.parentSheetName != null) {
				nbrParents++;
			}
		}

		if (nbrParents == 0) {
			return count;
		}
		/*
		 * any infinite loops with cyclical relationships?
		 */
		for (OutputRecord rec : this.outputRecords) {
			if (rec.parentSheetName != null) {
				count += this.validateParent(rec, allSheets, ctx);
			}
		}
		return count;
	}

	/**
	 * @return
	 */
	private int validateParent(OutputRecord outRec, Map allSheets, ValidationContext ctx) {
		/*
		 * check for existence of parent, as well
		 */
		Set parents = new HashSet();
		String sheet = outRec.sheetName;
		String parent = outRec.parentSheetName;
		while (true) {
			OutputRecord rec = allSheets.get(parent);
			/*
			 * do we have the parent?
			 */
			if (rec == null) {
				ctx.addError("output sheet " + sheet + " uses parentSheetName=" + parent
						+ " but that sheet name is not used in any outputRecord. Note that all sheets that aprticipate iin parent-child relationship must be defined using outputRecord elements.");
				return 1;
			}
			/*
			 * are we cycling in a circle?
			 */
			if (parents.add(parent) == false) {
				ctx.addError("output record with sheetName=" + sheet + " has its parentSheetName set to " + parent
						+ ". This is creating a cyclical child-parent relationship.");
				return 1;
			}
			/*
			 * is the chain over?
			 */
			if (rec.parentSheetName == null) {
				/*
				 * we are fine with this outoutRecord
				 */
				return 0;
			}
			sheet = rec.sheetName;
			parent = rec.parentSheetName;
		}
	}

	/**
	 * @param fields
	 * @return
	 */
	boolean okToOutputFieldsFromRecord(Field[] fields) {
		if (this.fieldNames == null || this.fieldNames.length == 0 || fields == null || fields.length == 0) {
			return true;
		}
		Set allNames = new HashSet(this.fieldNames.length);
		for (String aName : this.fieldNames) {
			allNames.add(aName);
		}
		for (Field field : fields) {
			if (allNames.contains(field.getName())) {
				return false;
			}
		}
		return true;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy