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

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

The newest version!
/*
 * Copyright (c) 2017 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, TORT 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.io.File;

import javax.transaction.UserTransaction;

import org.simplity.jms.JmsConnector;
import org.simplity.jms.JmsUsage;
import org.simplity.kernel.Application;
import org.simplity.kernel.ApplicationError;
import org.simplity.kernel.Tracer;
import org.simplity.kernel.comp.ValidationContext;
import org.simplity.kernel.db.DbAccessType;
import org.simplity.kernel.db.DbClientInterface;
import org.simplity.kernel.db.DbDriver;
import org.simplity.kernel.db.MultiTransClientInterface;
import org.simplity.kernel.util.TextUtil;
import org.simplity.kernel.value.Value;
import org.simplity.service.JavaAgent;
import org.simplity.service.ServiceContext;
import org.simplity.service.ServiceData;

/**
 * @author simplity.org
 *
 */
public class BatchProcessor extends Action {
	private static final String FIELD_PREFIX = "$";
	private static final String FOLDER_SEP = "/";

	/**
	 * folder where input files are expected.This folder is used for all file
	 * processors. Any input file that is expected in a different folder may
	 * have the relative folder path in its name
	 *
	 */
	String inputFolder;

	/**
	 * folder where output files are to be written out.This folder is used for all file
	 * processors. Any output file that is expected in a different folder may
	 * have the relative folder path in its name
	 *
	 */
	String outputFolder;

	/**
	 * main file processor
	 */
	BatchRowProcessor batchRowProcessor;

	/**
	 * What action do we take in case the input row fails data-type validation?
	 */
	String serviceOnInvalidInput;

	/**
	 * action to be executed if the row processing generates error
	 */
	String serviceOnErrorAtRowLevel;

	/**
	 * in case this thread is interrupted, should we exit after completing the
	 * current file?
	 */
	boolean exitOnInterrupt;
	/**
	 * do we use JMS?
	 */
	JmsUsage jmsUsage;

	/**
	 * what kind of rdbms access is required?
	 */
	DbAccessType dbAccessType;

	/**
	 * Does this use a schema different from the default schema for the project?
	 */
	String schemaName;
	/**
	 * if some custom code need to make use of the actual file name, let us set a field name in context
	 */
	String setActualFileNameTo;

	/**
	 * sub-service created for the desired service
	 */
	private SubService actionOnInvalidInput;
	/**
	 * sub-service created for the desired service
	 */
	private SubService actionOnErrorAtRowLevel;

	Action getInvalidAction() {
		return this.actionOnInvalidInput;
	}

	Action getErrorAction() {
		return this.actionOnErrorAtRowLevel;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.simplity.tp.Action#getDataAccessType()
	 * we manage our own db access, and not depend on what the service has set
	 */
	@Override
	public DbAccessType getDataAccessType() {
		return DbAccessType.NONE;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.simplity.tp.Action#getReady(int)
	 */
	@Override
	public void getReady(int idx, Service service) {
		super.getReady(idx, service);
		if (this.batchRowProcessor == null) {
			throw new ApplicationError("fileProcessor is required for a BatchProcessor");
		}

		this.batchRowProcessor.getReady(service);

		if (this.inputFolder == null) {
			if (this.batchRowProcessor.inputFile != null) {
				throw new ApplicationError("Batch processor uses input files, but inputFolder is not specified");
			}
		}

		if (this.outputFolder == null) {
			if (this.batchRowProcessor.outputFile != null) {
				throw new ApplicationError("Batch processor uses output files, but outputFolder is not specified");
			}
		}

		/*
		 * create and getReady() event actions if required
		 */
		if (this.serviceOnInvalidInput != null) {
			this.actionOnInvalidInput = new SubService();
			this.actionOnInvalidInput.serviceName = this.serviceOnInvalidInput;
			this.actionOnInvalidInput.getReady(0, service);
		}
		if (this.serviceOnErrorAtRowLevel != null) {
			this.actionOnErrorAtRowLevel = new SubService();
			this.actionOnErrorAtRowLevel.serviceName = this.serviceOnErrorAtRowLevel;
			this.actionOnErrorAtRowLevel.getReady(0, service);
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.simplity.tp.Block#validate(org.simplity.kernel.comp.
	 * ValidationContext, org.simplity.tp.Service)
	 */
	@Override
	public int validate(ValidationContext vtx, Service service) {
		int count = super.validate(vtx, service);
		if (this.actionOnErrorAtRowLevel != null) {
			count += this.actionOnErrorAtRowLevel.validate(vtx, service);
		}
		if (this.actionOnInvalidInput != null) {
			count += this.actionOnInvalidInput.validate(vtx, service);
		}
		count += this.batchRowProcessor.validate(vtx, service);
		return count;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.simplity.tp.Action#delegate(org.simplity.service.ServiceContext,
	 * org.simplity.kernel.db.DbDriver)
	 */
	@Override
	protected Value delegate(ServiceContext ctx, DbDriver driver) {
		String inFolderName = this.translateFolderName(this.inputFolder, ctx);
		String outFolderName = this.translateFolderName(this.outputFolder, ctx);
		File[] files = null;
		if (this.batchRowProcessor.inputFile == null) {
			/*
			 * sql driven. eqvt of one null file to be processed
			 */
			files = new File[1];
			// files[0] remain as null
		} else {
			files = this.getFiles(inFolderName, ctx);
		}

		if (files == null) {
			Tracer.trace("BatchProcesser " + this.actionName + " had nothing to process.");
			return Value.VALUE_ZERO;
		}
		/*
		 * resources that need to be released without fail..
		 */
		JmsConnector jmsConnector = null;
		UserTransaction userTransaction = null;
		/*
		 * Simplity has made big issue out of db connection, and insists on not
		 * using the connection explicitly. Best practice is to use a call back
		 * object that is provided with an instance of dbDriver for a method. We
		 * use a worker for that sake
		 */

		ApplicationError exception = null;
		int nbrRows = 0;
		try {
			/*
			 * acquire resources that are needed for this service
			 */
			if (this.jmsUsage != null) {
				jmsConnector = JmsConnector.borrowMultiTransConnector(this.jmsUsage);
				ctx.setJmsSession(jmsConnector.getSession());
			}

			/*
			 * jms connector is sent to worker only if needs to be used for
			 * transaction management at each row
			 */
			JmsConnector con = null;
			if (this.jmsUsage == JmsUsage.SERVICE_MANAGED) {
				con = jmsConnector;
			}
			Worker worker = new Worker(inFolderName, outFolderName, con, userTransaction, files, ctx);
			worker.work();
			nbrRows = worker.getNbrRowsProcessed();
		} catch (ApplicationError e) {
			exception = e;
		} catch (Exception e) {
			exception = new ApplicationError(e, "Exception during execution of service. ");
		}
		/*
		 * close/return resources
		 */
		if (jmsConnector != null) {
			JmsConnector.returnConnector(jmsConnector, true);
		}
		if (exception == null) {
			return Value.newIntegerValue(nbrRows);
		}
		throw exception;
	}

	private String translateFolderName(String folder, ServiceContext ctx) {

		String folderName = folder;
		if (folderName == null) {
			return null;
		}

		if (folderName.startsWith(FIELD_PREFIX)) {
			/*
			 * get it as a field from context
			 */
			folderName = ctx.getTextValue(folderName.substring(1));
			if (folderName == null) {
				throw new ApplicationError(
						"No value for folder name field " + folder.substring(1) + " in service context");
			}
		}

		if (folderName.endsWith(FOLDER_SEP) == false) {
			folderName += FOLDER_SEP;
		}
		return folderName;
	}

	/**
	 * @param ctx
	 * @return
	 */
	private File[] getFiles(String folderName, ServiceContext ctx) {

		File folder = new File(folderName);
		if (folder.exists() == false) {
			throw new ApplicationError("No folder named " + folderName + "and hence batch processor cannot proceed.");
		}

		Tracer.trace("Going to process files in folder " + folderName);

		String fileName = this.batchRowProcessor.inputFile.fileName;
		if (fileName.startsWith(FIELD_PREFIX) == false) {
			/*
			 * it is a pattern, and not a field name.
			 * we are possibly reading multiple files with this pattern
			 */
			File[] files = folder.listFiles(TextUtil.getFileNameFilter(fileName));
			if (files == null || files.length == 0) {
				Tracer.trace("No file found in folder " + this.inputFolder + " matching name " + fileName
						+ ". Batch processor has no work.");
				return null;
			}
			return files;
		}
		/*
		 * get it as a field from context. We are processing just that file
		 */
		fileName = ctx.getTextValue(fileName.substring(1));
		if (fileName == null) {
			throw new ApplicationError(
					"input file name is to be from field " + this.batchRowProcessor.inputFile.fileName.substring(1)
							+ " but no value is foundin service context for thsi field");
		}
		File[] files = { new File(folderName + fileName) };
		return files;
	}

	/**
	 * we create a worker for this component because we need to keep state
	 * across method calls. Our main class remains state-less (extremely
	 * important to ensure thread-safety)
	 *
	 * @author simplity.org
	 *
	 */
	class Worker implements DbClientInterface, MultiTransClientInterface {
		/*
		 * state-less instance attributes
		 */
		final JmsConnector jmsConnector;
		final UserTransaction userTransaction;
		final File[] files;
		final ServiceContext ctx;
		final String inFolderName;
		final String outFolderName;

		/*
		 * state-attribute
		 */
		int nbrRowsProcessed;

		/**
		 * @param inName
		 * @param outName
		 * @param jmsConnector
		 * @param userTransaction
		 * @param files
		 * @param ctx
		 */
		public Worker(String inName, String outName, JmsConnector jmsConnector, UserTransaction userTransaction, File[] files,
				ServiceContext ctx) {
			this.inFolderName = inName;
			this.outFolderName = outName;
			this.jmsConnector = jmsConnector;
			this.userTransaction = userTransaction;
			this.files = files;
			this.ctx = ctx;
		}

		/**
		 * @return number of rows processed by this worker
		 */
		public int getNbrRowsProcessed() {
			return this.nbrRowsProcessed;
		}

		/**
		 * this is an intermediate method to get the dbDriver before executing
		 * our real worker method named processAllFiles()
		 */
		void work() {
			DbAccessType access = BatchProcessor.this.dbAccessType;
			if (access == null || access == DbAccessType.NONE) {
				this.processAllFiles(null);
				return;
			}

			String schema = BatchProcessor.this.schemaName;
			if (access == DbAccessType.READ_WRITE) {
				/*
				 * dbDriver will call us back doMultiplTrans();
				 */
				DbDriver.workForMultiTrans(this, schema);
				return;
			}

			if (access == DbAccessType.SUB_SERVICE) {
				access = DbAccessType.READ_ONLY;
			}
			/*
			 * db driver will call us back with workWithDriver()
			 */
			DbDriver.workWithDriver(this, access, schema);
		}

		@Override
		public int doMultiplTrans(DbDriver dbDriver) {
			/*
			 * we got the dbDriver. go to main processing
			 */
			this.processAllFiles(dbDriver);
			return this.nbrRowsProcessed;
		}

		@Override
		public boolean workWithDriver(DbDriver dbDriver) {
			/*
			 * we got the dbDriver. go to main processing
			 */
			this.processAllFiles(dbDriver);
			return false;
		}

		/**
		 * real worker method that does the core job. Process all the files
		 * using the resources that are made available
		 */
		private void processAllFiles(DbDriver dbDriver) {
			for (File file : this.files) {
				String actualName = null;
				if(file != null){
					 file.getName();
				}
				if(BatchProcessor.this.setActualFileNameTo != null){
					this.ctx.setTextValue(BatchProcessor.this.setActualFileNameTo, actualName);
				}
				try {
					this.nbrRowsProcessed += BatchProcessor.this.batchRowProcessor.process(file, this, dbDriver, this.ctx, BatchProcessor.this.exitOnInterrupt);
				} catch (Exception e) {
					Action action = BatchProcessor.this.getErrorAction();
					if (action == null) {
						Application.reportApplicationError(null,
								new ApplicationError(e, "Error while processing a file in batchProcessor"));
					} else {
						try {
							action.act(this.ctx, null);
						} catch (Exception ex) {
							Application.reportApplicationError(null, ex);
						}
					}
				}
			}
		}

		/**
		 * called-back from the primary file processor before processing each
		 * row
		 */
		void beginTrans() {
			if (this.userTransaction != null) {
				try {
					this.userTransaction.begin();
				} catch (Exception e) {
					throw new ApplicationError(e, "Unable to use begin() on  user transaction  on on an instance of "
							+ this.userTransaction.getClass().getName());
				}
			}
		}

		/**
		 * called-back from primary file-processor at the end of each row.
		 * Essentially handle commit/rollBack and exception reporting
		 *
		 * @param exception
		 * @param driver
		 */
		void endTrans(Exception exception, DbDriver driver) {
			boolean allOk = exception == null && this.ctx.isInError() == false;
			if (this.userTransaction != null) {
				try {
					if (allOk) {
						this.userTransaction.commit();
					} else {
						this.userTransaction.rollback();
					}
				} catch (Exception ignore) {
					//
				}
			} else {
				/*
				 * service managed transactions
				 */
				if (this.jmsConnector != null) {
					try {
						if (allOk) {
							this.jmsConnector.commit();
						} else {
							this.jmsConnector.rollback();
						}
					} catch (Exception ignore) {
						//
					}
				}

				if (BatchProcessor.this.dbAccessType == DbAccessType.READ_WRITE) {
					try {
						if (allOk) {
							driver.commit();
						} else {
							driver.rollback();
						}
					} catch (Exception ignore) {
						//
					}
				}
			}
			if (exception == null) {
				return;
			}

			/*
			 * invalid data in a row
			 */
			if (exception instanceof InvalidRowException) {
				this.errorOnInputValidation(exception);
				return;
			}
			/*
			 * general error
			 */
			Action action = BatchProcessor.this.getErrorAction();
			if (action == null) {
				Application.reportApplicationError(null, exception);
			} else {
				try {
					/*
					 * this is a sub-service, and we want it to use its own
					 * driver
					 */
					action.act(this.ctx, null);
				} catch (Exception ex) {
					Application.reportApplicationError(null, ex);
				}
			}
		}

		/**
		 * input row was in error
		 * @param exception
		 */
		void errorOnInputValidation(Exception exception){
			Action action = BatchProcessor.this.getInvalidAction();
			if (action == null) {
				Application.reportApplicationError(null,
						new ApplicationError(exception, "Input file contains invalid data for batch processor"));
			} else {
				try {
					/*
					 * this is a sub-service, and we want it to use its own
					 * driver
					 */
					action.act(this.ctx, null);
				} catch (Exception ex) {
					Application.reportApplicationError(null, ex);
				}
			}
		}
	}
/**
 *
 * @param args
 * @throws Exception
 */
	public static void main(String[] args) throws Exception {
		String comp = "c:/repos/simplity/examples/WebContent/WEB-INF/comp/";
		Application.bootStrap(comp);
		String serviceName = "file.po";
		JavaAgent agent = JavaAgent.getAgent("420", "pwd");
		ServiceData outData = agent.serve(serviceName, "");
		System.out.println(outData.getPayLoad());
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy