/*
 * Koekiebox CONFIDENTIAL
 *
 * [2012] - [2017] Koekiebox (Pty) Ltd
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property
 * of Koekiebox and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Koekiebox
 * and its suppliers and may be covered by South African and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material is strictly
 * forbidden unless prior written permission is obtained from Koekiebox.
 */

package com.fluidbpm.program.api.util.elasticsearch;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.client.Client;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.json.JSONObject;

import com.fluidbpm.program.api.util.ABaseUtil;
import com.fluidbpm.program.api.util.cache.CacheUtil;
import com.fluidbpm.program.api.util.elasticsearch.exception.FluidElasticSearchException;
import com.fluidbpm.program.api.util.sql.ABaseSQLUtil;
import com.fluidbpm.program.api.util.sql.impl.SQLFormFieldUtil;
import com.fluidbpm.program.api.vo.ABaseFluidJSONObject;
import com.fluidbpm.program.api.vo.field.Field;
import com.fluidbpm.program.api.vo.field.TableField;
import com.fluidbpm.program.api.vo.form.Form;

/**
 * ElasticSearch base Utility class.
 *
 * @author jasonbruwer on 2016/08/19.
 * @since 1.3
 * @see ABaseUtil
 * @see ABaseSQLUtil
 */
public abstract class ABaseESUtil extends ABaseSQLUtil {

	protected Client client;
	protected SQLFormFieldUtil fieldUtil = null;

	protected static final int DEFAULT_OFFSET = 0;
	protected static final int MAX_NUMBER_OF_TABLE_RECORDS = 10000;

	//public static final String[] NO_FIELDS_MAPPER = {"_none_"};
	public static final String[] NO_FIELDS_MAPPER = {"_id","_type"};

	/**
	 * The index type.
	 */
	public static final class Index {

		public static final String DOCUMENT = "document";
		public static final String FOLDER = "folder";
		public static final String TABLE_RECORD = "table_record";

		/**
		 * Checks to see whether {@code indexParam} is a valid
		 * index type.
		 *
		 * @param indexParam The index to check.
		 * @return {@code true} for valid index and {@code false} for invalid index.
		 */
		public static boolean isIndexValid(String indexParam)
		{
			if(DOCUMENT.equals(indexParam))
			{
				return true;
			}
			else if(FOLDER.equals(indexParam))
			{
				return true;
			}
			else if(TABLE_RECORD.equals(indexParam))
			{
				return true;
			}

			return false;
		}
	}

	/**
	 * Initialise with the ElasticSearch client.
	 *
	 * @param connectionParam SQL Connection to use.
	 * @param esClientParam The ES Client.
	 * @param cacheUtilParam The Cache Util for better performance.
	 */
	public ABaseESUtil(
			Connection connectionParam,
			Client esClientParam,
			CacheUtil cacheUtilParam) {

		super(connectionParam, cacheUtilParam);
		this.client = esClientParam;
		this.fieldUtil = new SQLFormFieldUtil(connectionParam, cacheUtilParam);
	}

	/**
	 * Initialise with the ElasticSearch client.
	 *
	 * @param esClientParam The ES Client.
	 */
	public ABaseESUtil(Client esClientParam) {
		super(null);
		this.client = esClientParam;
	}

	/*******************************************************/
	/**
	 * Performs a search against the Elasticsearch instance with the {@code qbParam}.
	 *
	 * @param qbParam The @{code QueryBuilder} to search with.
	 * @param indexParam The Elasticsearch Index to use {@code (document, folder or table_field)}.
	 * @param offsetParam The offset for the results to return.
	 * @param limitParam The number of results to return.
	 * @param formTypesParam The Id's for the Form Definitions to be returned.
	 * @return The {@code ElasticTypeAndId} {@code List}.
	 *
	 * @see Index
	 * @see ElasticTypeAndId
	 */
	public final List<ElasticTypeAndId> searchAndConvertHitsToIdsOnly(
			QueryBuilder qbParam,
			String indexParam,
			int offsetParam,
			int limitParam,
			Long ... formTypesParam) {

		SearchHits searchHits = this.searchWithHits(
				qbParam,
				indexParam,
				true,
				offsetParam,
				limitParam,
				formTypesParam);

		List<ElasticTypeAndId> returnVal = null;

		long totalHits;
		if (searchHits != null && (totalHits = searchHits.getTotalHits()) > 0) {

			returnVal = new ArrayList();

			if((searchHits.getHits().length != totalHits) &&
					(searchHits.getHits().length != limitParam))
			{
				throw new FluidElasticSearchException(
						"The Hits and fetch count has mismatch. Total hits is '"+totalHits
								+"' while hits is '"+
								searchHits.getHits().length+"'.");
			}

			long iterationMax = totalHits;
			if(limitParam > 0 && totalHits > limitParam) {
				iterationMax = limitParam;
			}

			//Iterate...
			for(int index = 0;index < iterationMax;index++) {
				SearchHit searchHit = searchHits.getAt(index);
				String idAsString;
				if((idAsString = searchHit.getId()) == null) {
					continue;
				}

				returnVal.add(new ElasticTypeAndId(
						this.toLongSafe(idAsString),
						searchHit.getType()));
			}
		}

		return returnVal;
	}

	/**
	 * Performs a search against the Elasticsearch instance with the {@code qbParam}.
	 *
	 * @param qbParam The @{code QueryBuilder} to search with.
	 * @param indexParam The Elasticsearch Index to use {@code (document, folder or table_field)}.
	 * @param withNoFieldsParam Should fields be IGNORED returned.
	 * @param offsetParam The offset for the results to return.
	 * @param limitParam The max number of results to return.
	 * @param formTypesParam The Id's for the Form Definitions to be returned.
	 *
	 * @return The Elasticsearch {@code SearchHits}.
	 */
	public final SearchHits searchWithHits(
			QueryBuilder qbParam,
			String indexParam,
			boolean withNoFieldsParam,
			int offsetParam,
			int limitParam,
			Long ... formTypesParam
	) {
		if(this.client == null) {
			throw new ElasticsearchException("Elasticsearch client is not set.");
		}

		SearchRequestBuilder searchRequestBuilder = this.client.prepareSearch(
				//Indexes...
				indexParam)
				.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
				.setQuery(qbParam)
				.setFrom(0)
				.setExplain(false);

		//No Fields...
		if (withNoFieldsParam) {
			searchRequestBuilder = searchRequestBuilder.storedFields(NO_FIELDS_MAPPER);
		}

		//The requested number of results...
		if(limitParam > 0)
		{
			searchRequestBuilder = searchRequestBuilder.setSize(limitParam);
		}

		if(offsetParam > -1){
			searchRequestBuilder = searchRequestBuilder.setFrom(offsetParam);
		}

		if(formTypesParam == null)
		{
			formTypesParam = new Long[]{};
		}

		//If Types is set...
		if(formTypesParam != null && formTypesParam.length > 0)
		{
			String[] formTypesAsString = new String[formTypesParam.length];
			for(int index = 0;index < formTypesParam.length;index++)
			{
				Long formTypeId = formTypesParam[index];
				if(formTypeId == null)
				{
					continue;
				}

				formTypesAsString[index] = formTypeId.toString();
			}
			searchRequestBuilder = searchRequestBuilder.setTypes(formTypesAsString);
		}

		//Perform the actual search...
		SearchResponse searchResponse = searchRequestBuilder.execute().actionGet();

		if (searchResponse == null) {
			return null;
		}

		return searchResponse.getHits();
	}

	/**
	 * Performs a search against the Elasticsearch instance with the {@code qbParam}.
	 *
	 * @param qbParam The @{code QueryBuilder} to search with.
	 * @param indexParam The Elasticsearch Index to use {@code (document, folder or table_field)}.
	 * @param withNoFieldsParam Should fields be IGNORED returned.
	 * @param offsetParam The offset for the results to return.
	 * @param limitParam The number of results to return.
	 * @param formTypesParam The Id's for the Form Definitions to be returned.
	 *
	 * @return {@code true} if there were any {@code SearchHits} from the lookup.
	 */
	public final boolean searchContainHits(
			QueryBuilder qbParam,
			String indexParam,
			boolean withNoFieldsParam,
			int offsetParam,
			int limitParam,
			Long ... formTypesParam) {

		SearchHits searchHits = this.searchWithHits(
				qbParam,
				indexParam,
				withNoFieldsParam,
				offsetParam,
				limitParam,
				formTypesParam);

		return (searchHits != null && searchHits.getTotalHits() > 0);
	}

	/**
	 * Retrieves the {@code Form}'s via the provided {@code formIdsParam}.
	 *
	 * @param indexParam The ElasticSearch index to be used for lookup.
	 * @param formIdsParam {@code List} of Identifiers for the Forms to return.
	 * @param includeFieldDataParam Whether to populate the return {@code Form} table fields.
	 * @param offsetParam The offset for the results to return.
	 * @param limitParam The max number of results to return.
	 *
	 * @return {@code List<Form>} List of Forms.
	 */
	public final List<Form> getFormsByIds(
			String indexParam,
			List<Long> formIdsParam,
			boolean includeFieldDataParam,
			int offsetParam,
			int limitParam
	) {
		if(formIdsParam == null || formIdsParam.isEmpty()) {
			return null;
		}

		if(indexParam == null || indexParam.trim().isEmpty()) {
			throw new FluidElasticSearchException(
					"Index is mandatory for lookup.");
		}

		//Query using the descendantId directly...
		StringBuffer byIdQuery = new StringBuffer();

		for(Long formId : formIdsParam) {
			byIdQuery.append(ABaseFluidJSONObject.JSONMapping.ID);
			byIdQuery.append(":\"");
			byIdQuery.append(formId);
			byIdQuery.append("\" ");
		}

		String queryByIdsToString = byIdQuery.toString();
		queryByIdsToString = queryByIdsToString.substring(0, queryByIdsToString.length() - 1);

		List<Form> returnVal = null;
		if(includeFieldDataParam) {
			returnVal = this.searchAndConvertHitsToFormWithAllFields(
					QueryBuilders.queryStringQuery(queryByIdsToString),
					indexParam,
					offsetParam,
					limitParam,
					new Long[]{});
		} else {
			returnVal = this.searchAndConvertHitsToFormWithNoFields(
					QueryBuilders.queryStringQuery(queryByIdsToString),
					indexParam,
					offsetParam,
					limitParam,
					new Long[]{});
		}

		if(returnVal == null || returnVal.isEmpty()) {
			return null;
		}

		return returnVal;
	}

	/**
	 * Performs a search against the Elasticsearch instance with the {@code qbParam}.
	 *
	 * @param qbParam The @{code QueryBuilder} to search with.
	 * @param indexParam The Elasticsearch Index to use {@code (document, folder or table_field)}.
	 * @param offsetParam The offset for the results to return.
	 * @param limitParam The max number of results to return.
	 * @param formTypesParam The Id's for the Form Definitions to be returned.
	 *
	 * @return The {@code SearchHits} as Fluid {@code Form}.
	 *
	 * @see Form
	 * @see SearchHits
	 */
	public final List<Form> searchAndConvertHitsToFormWithAllFields(
			QueryBuilder qbParam,
			String indexParam,
			int offsetParam,
			int limitParam,
			Long ... formTypesParam
	) {
		SearchHits searchHits = this.searchWithHits(
				qbParam,
				indexParam,
				false,
				offsetParam,
				limitParam,
				formTypesParam);

		List<Form> returnVal = null;

		long totalHits;
		if (searchHits != null && (totalHits = searchHits.getTotalHits()) > 0) {
			returnVal = new ArrayList();

			if((searchHits.getHits().length != totalHits) &&
					(searchHits.getHits().length != limitParam)) {
				throw new FluidElasticSearchException(
						"The Hits and fetch count has mismatch. Total hits is '"+totalHits+"' while hits is '"+
								searchHits.getHits().length+"'.");
			}

			long iterationMax = totalHits;
			if(limitParam > 0 && totalHits > limitParam) {
				iterationMax = limitParam;
			}

			//Iterate...
			for(int index = 0;index < iterationMax;index++) {
				SearchHit searchHit = searchHits.getAt(index);

				String source;
				if((source = searchHit.getSourceAsString()) == null) {
					continue;
				}

				this.printInfoOnSourceFromES(searchHit);

				Form formFromSource = new Form();

				JSONObject jsonObject = new JSONObject(source);
				List<Field> fieldsForForm = null;
				//Is Form Type available...
				if(jsonObject.has(Form.JSONMapping.FORM_TYPE_ID)) {
					if(this.fieldUtil == null) {
						throw new FluidElasticSearchException(
								"Field Util is not set. Use a different constructor.");
					}

					fieldsForForm = formFromSource.convertTo(
							this.fieldUtil.getFormFieldMappingForFormDefinition(
									jsonObject.getLong(Form.JSONMapping.FORM_TYPE_ID)));
				}

				formFromSource.populateFromElasticSearchJson(
						jsonObject, fieldsForForm);

				returnVal.add(formFromSource);
			}
		}

		return returnVal;
	}

	/**
	 * Performs a search against the Elasticsearch instance with the {@code qbParam}.
	 *
	 * @param qbParam The @{code QueryBuilder} to search with.
	 * @param indexParam The Elasticsearch Index to use {@code (document, folder or table_field)}.
	 * @param offsetParam The offset for the results to return.
	 * @param limitParam The max number of results to return.
	 * @param formTypesParam The Id's for the Form Definitions to be returned.
	 *
	 * @return The {@code SearchHits} as Fluid {@code Form}.
	 *
	 * @see Form
	 * @see SearchHits
	 */
	public final List<Form> searchAndConvertHitsToFormWithNoFields(
			QueryBuilder qbParam,
			String indexParam,
			int offsetParam,
			int limitParam,
			Long ... formTypesParam
	) {
		SearchHits searchHits = this.searchWithHits(
				qbParam,
				indexParam,
				false,
				offsetParam,
				limitParam,
				formTypesParam);

		List<Form> returnVal = null;

		long totalHits;
		if (searchHits != null && (totalHits = searchHits.getTotalHits()) > 0) {
			returnVal = new ArrayList();

			if((searchHits.getHits().length != totalHits) &&
					(searchHits.getHits().length != limitParam)) {
				throw new FluidElasticSearchException(
						"The Hits and fetch count has mismatch. Total hits is '"+
								totalHits+"' while hits is '"+
								searchHits.getHits().length+"'.");
			}

			long iterationMax = totalHits;
			if(limitParam > 0 && totalHits > limitParam) {
				iterationMax = limitParam;
			}

			//Iterate...
			for(int index = 0;index < iterationMax;index++) {
				SearchHit searchHit = searchHits.getAt(index);

				String source;
				if((source = searchHit.getSourceAsString()) == null) {
					continue;
				}

				this.printInfoOnSourceFromES(searchHit);

				Form formFromSource = new Form();

				formFromSource.populateFromElasticSearchJson(
						new JSONObject(source),
						null);

				returnVal.add(formFromSource);
			}
		}

		return returnVal;
	}

	/**
	 * Performs a search against the Elasticsearch instance with the {@code qbParam}.
	 *
	 * @param qbParam The @{code QueryBuilder} to search with.
	 * @param indexParam The Elasticsearch Index to use {@code (document, folder or table_field)}.
	 * @param fieldsParam The Fluid {@code Field}'s to return after lookup.
	 * @param offsetParam The offset for the results to return.
	 * @param limitParam The max number of results to return.
	 * @param formTypesParam The Id's for the Form Definitions to be returned.
	 *
	 * @return The {@code SearchHits} as Fluid {@code Form}.
	 *
	 * @see Form
	 * @see SearchHits
	 */
	public List<Form> searchAndConvertHitsToForm(
			QueryBuilder qbParam,
			String indexParam,
			List<Field> fieldsParam,
			int offsetParam,
			int limitParam,
			Long ... formTypesParam
	) {
		SearchHits searchHits = this.searchWithHits(
				qbParam,
				indexParam,
				false,
				offsetParam,
				limitParam,
				formTypesParam);

		List<Form> returnVal = null;

		long totalHits;
		if (searchHits != null && (totalHits = searchHits.getTotalHits()) > 0) {
			returnVal = new ArrayList();

			if((searchHits.getHits().length != totalHits) &&
					(searchHits.getHits().length != limitParam)) {
				throw new FluidElasticSearchException(
						"The Hits and fetch count has mismatch. Total hits is '"+totalHits
								+"' while hits is '"+
								searchHits.getHits().length+"'.");
			}

			long iterationMax = totalHits;
			if(limitParam > 0 && totalHits > limitParam) {
				iterationMax = limitParam;
			}

			//Iterate...
			for(int index = 0;index < iterationMax;index++) {
				SearchHit searchHit = searchHits.getAt(index);

				String source;
				if((source = searchHit.getSourceAsString()) == null) {
					continue;
				}

				this.printInfoOnSourceFromES(searchHit);

				Form formFromSource = new Form();

				formFromSource.populateFromElasticSearchJson(
						new JSONObject(source),
						fieldsParam);

				returnVal.add(formFromSource);
			}
		}

		return returnVal;
	}

	/**
	 * Populate all the Table Field values from the Table index.
	 *
	 * @param addAllTableRecordsForReturnParam Whether to include all the Table Records as a return value.
	 * @param includeFieldDataParam Whether to populate the return {@code Form} table fields.
	 * @param formFieldsParam The {@code Field}s to populate.
	 *
	 * @return {@code List<Form>} of populated Table Field records.
	 */
	protected final List<Form> populateTableFields(
			boolean addAllTableRecordsForReturnParam,
			boolean includeFieldDataParam,
			List<Field> formFieldsParam) {
		if(formFieldsParam == null || formFieldsParam.isEmpty()) {
			return null;
		}

		List<Form> allTableRecordsFromAllFields = addAllTableRecordsForReturnParam ?
				new ArrayList() : null;

		//Populate each of the Table Fields...
		for(Field descendantField : formFieldsParam) {
			//Skip if not Table Field...
			if(!(descendantField.getFieldValue() instanceof TableField)) {
				continue;
			}

			TableField tableField = (TableField)descendantField.getFieldValue();

			List<Form> tableRecordWithIdOnly = tableField.getTableRecords();
			if(tableRecordWithIdOnly == null || tableRecordWithIdOnly.isEmpty()) {
				continue;
			}

			//Populate the ids for lookup...
			List<Long> formIdsOnly = new ArrayList();
			for(Form tableRecord : tableRecordWithIdOnly) {
				formIdsOnly.add(tableRecord.getId());
			}

			List<Form> populatedTableRecords = this.getFormsByIds(
					Index.TABLE_RECORD,
					formIdsOnly,
					includeFieldDataParam,
					DEFAULT_OFFSET, MAX_NUMBER_OF_TABLE_RECORDS);

			if(addAllTableRecordsForReturnParam && populatedTableRecords != null) {
				allTableRecordsFromAllFields.addAll(populatedTableRecords);
			}

			tableField.setTableRecords(populatedTableRecords);
			descendantField.setFieldValue(tableField);
		}

		return allTableRecordsFromAllFields;
	}

	/**
	 * Confirms whether index with the name {@code indexToCheckParam} exists.
	 *
	 * @param indexToCheckParam ElasticSearch index to check for existance.
	 * @return {@code true} if ElasticSearch index {@code indexToCheckParam} exists, otherwise {@code false}.
	 */
	public boolean doesIndexExist(String indexToCheckParam) {
		if(indexToCheckParam == null || indexToCheckParam.trim().isEmpty()) {
			return false;
		}

		if(this.client == null) {
			throw new FluidElasticSearchException(
					"ElasticSearch client is not initialized.");
		}

		return this.client.admin().cluster()
				.prepareState().execute()
				.actionGet().getState()
				.getMetaData().hasIndex(indexToCheckParam);
	}

	/**
	 * Close the SQL and ElasticSearch Connection.
	 */
	@Override
	public void closeConnection() {

		CloseConnectionRunnable closeConnectionRunnable =
				new CloseConnectionRunnable(this);

		Thread closeConnThread = new Thread(
				closeConnectionRunnable,"Close ABaseES Connection");
		closeConnThread.start();
	}

	/**
	 * Close the SQL and ElasticSearch Connection, but not in
	 * a separate {@code Thread}.
	 */
	public void closeConnectionNonThreaded() {
		super.closeConnection();

		if(this.client != null) {
			this.client.close();
		}

		this.client = null;
	}

	/**
	 * Utility class to close the connection in a thread.
	 */
	private static class CloseConnectionRunnable implements Runnable {

		private ABaseESUtil baseESUtil;

		/**
		 * The resource to close.
		 *
		 * @param baseESUtilParam Base utility to close.
		 */
		public CloseConnectionRunnable(ABaseESUtil baseESUtilParam) {
			this.baseESUtil = baseESUtilParam;
		}

		/**
		 * Performs the threaded operation.
		 */
		@Override
		public void run() {
			if(this.baseESUtil == null) {
				return;
			}

			this.baseESUtil.closeConnectionNonThreaded();
		}
	}

	/**
	 * Do nothing.
	 * @param searchHitParam Search Hit to perform functions on.
	 */
	private void printInfoOnSourceFromES(SearchHit searchHitParam) { }
}


/*
 * Koekiebox CONFIDENTIAL
 *
 * [2012] - [2017] Koekiebox (Pty) Ltd
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property
 * of Koekiebox and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Koekiebox
 * and its suppliers and may be covered by South African and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material is strictly
 * forbidden unless prior written permission is obtained from Koekiebox.
 */

package com.fluidbpm.program.api.vo.attachment;

import java.io.File;
import java.util.Date;

import javax.xml.bind.annotation.XmlTransient;

import org.json.JSONException;
import org.json.JSONObject;

import com.fluidbpm.program.api.vo.ABaseFluidJSONObject;
import com.fluidbpm.program.api.vo.form.Form;
import com.fluidbpm.program.api.vo.item.FluidItem;

/**
 * Represents an Fluid Field for Form, User, Route and Global.
 *
 * {@code Field} can be part of Electronic Form or Form Definition in Fluid.
 *
 * @author jasonbruwer
 * @since v1.0
 *
 * @see Form
 * @see FluidItem
 * @see com.fluidbpm.program.api.vo.mail.MailMessage
 * @see com.fluidbpm.program.api.vo.mail.MailMessageAttachment
 */
public class Attachment extends ABaseFluidJSONObject {

    public static final long serialVersionUID = 1L;

    private String version;
    private String name;
    private String path;
    private String contentType;

    private Date dateLastUpdated;
    private Date dateCreated;

    private String attachmentDataBase64;

    private Long formId;

    /**
     * The JSON mapping for the {@code Attachment} object.
     */
    public static class JSONMapping {
        public static final String NAME = "name";
        public static final String PATH = "path";

        public static final String VERSION = "version";
        public static final String CONTENT_TYPE = "contentType";

        public static final String DATE_LAST_UPDATED = "dateLastUpdated";
        public static final String DATE_CREATED = "dateCreated";

        public static final String FORM_ID = "formId";

        public static final String ATTACHMENT_DATA_BASE64 = "attachmentDataBase64";
    }

    /**
     * Default constructor.
     */
    public Attachment() {
        super();
    }

    /**
     * Sets the Id associated with the Attachment.
     *
     * @param attachmentIdParam Attachment Id.
     */
    public Attachment(Long attachmentIdParam) {
        super();

        this.setId(attachmentIdParam);
    }

    /**
     * Sets the Path and Name of {@code this} {@code Attachment}.
     *
     * @param attachmentPath Sets the {@code Attachment} path.
     * @param attachmentNameParam Sets the {@code Attachment} name.
     */
    public Attachment(String attachmentPath, String attachmentNameParam) {
        super();
        this.setPath(attachmentPath);
        this.setName(attachmentNameParam);
    }

    /**
     * Sets the Path {@code this} {@code Attachment}.
     *
     * @param attachmentPath The path to the {@code Attachment}.
     */
    public Attachment(String attachmentPath) {
        super();
        this.setPath(attachmentPath);

        File theFile = new File(attachmentPath);
        if(theFile.exists() && theFile.isFile())
        {
            this.setName(theFile.getName());
        }
    }

    /**
     * Populates local variables with {@code jsonObjectParam}.
     *
     * @param jsonObjectParam The JSON Object.
     */
    public Attachment(JSONObject jsonObjectParam) {
        super(jsonObjectParam);

        if(this.jsonObject == null)
        {
            return;
        }

        //Name...
        if (!this.jsonObject.isNull(JSONMapping.NAME)) {
            this.setName(this.jsonObject.getString(JSONMapping.NAME));
        }

        //Path...
        if (!this.jsonObject.isNull(JSONMapping.PATH)) {
            this.setPath(this.jsonObject.getString(JSONMapping.PATH));
        }

        //Version...
        if (!this.jsonObject.isNull(JSONMapping.VERSION)) {
            this.setVersion(this.jsonObject.getString(JSONMapping.VERSION));
        }

        //Content Type...
        if (!this.jsonObject.isNull(JSONMapping.CONTENT_TYPE)) {
            this.setContentType(this.jsonObject.getString(JSONMapping.CONTENT_TYPE));
        }

        //Form Id...
        if (!this.jsonObject.isNull(JSONMapping.FORM_ID)) {
            this.setFormId(this.jsonObject.getLong(JSONMapping.FORM_ID));
        }

        //Date Created...
        if (!this.jsonObject.isNull(JSONMapping.DATE_CREATED)) {
            this.setDateCreated(new Date(
                    this.jsonObject.getLong(JSONMapping.DATE_CREATED)));
        }

        //Date Last Updated...
        if (!this.jsonObject.isNull(JSONMapping.DATE_LAST_UPDATED)) {
            this.setDateLastUpdated(new Date(
                    this.jsonObject.getLong(JSONMapping.DATE_LAST_UPDATED)));
        }

        //Attachment Data...
        if (!this.jsonObject.isNull(JSONMapping.ATTACHMENT_DATA_BASE64)) {
            this.setAttachmentDataBase64(this.jsonObject.getString(
                    JSONMapping.ATTACHMENT_DATA_BASE64));
        }
    }

    /**
     * Gets the version of {@code this} {@code Attachment}.
     *
     * @return Attachment version.
     */
    public String getVersion() {
        return this.version;
    }

    /**
     * Sets the version of {@code this} {@code Attachment}.
     *
     * @param versionParam Attachment version.
     */
    public void setVersion(String versionParam) {
        this.version = versionParam;
    }

    /**
     * Gets the name of {@code this} {@code Attachment}.
     *
     * @return Attachment name.
     */
    public String getName() {
        return this.name;
    }

    /**
     * Sets the name of {@code this} {@code Attachment}.
     *
     * @param nameParam Attachment name.
     */
    public void setName(String nameParam) {
        this.name = nameParam;
    }

    /**
     * Gets the path of {@code this} {@code Attachment}.
     *
     * @return Attachment path.
     */
    public String getPath() {
        return this.path;
    }

    /**
     * Sets the path of {@code this} {@code Attachment}.
     *
     * @param pathParam Attachment path.
     */
    public void setPath(String pathParam) {
        this.path = pathParam;
    }

    /**
     * Gets the id of {@code this} {@code Attachment} form parent.
     *
     * @return Attachment associated Form Id.
     */
    public Long getFormId() {
        return this.formId;
    }

    /**
     * Sets the id of {@code this} {@code Attachment} form parent.
     *
     * @param formIdParam Attachment associated Form Id.
     */
    public void setFormId(Long formIdParam) {
        this.formId = formIdParam;
    }

    /**
     * Gets the content-type of {@code this} {@code Attachment}.
     *
     * @return Attachment Content / Mime Type.
     */
    public String getContentType() {
        return this.contentType;
    }

    /**
     * Sets the content-type of {@code this} {@code Attachment}.
     *
     * @param contentTypeParam Attachment Content / Mime Type.
     */
    public void setContentType(String contentTypeParam) {
        this.contentType = contentTypeParam;
    }

    /**
     * Gets the {@code Date} when {@code this} {@code Attachment} was last updated.
     *
     * @return Attachment Last Updated Date.
     */
    public Date getDateLastUpdated() {
        return this.dateLastUpdated;
    }

    /**
     * Sets the {@code Date} when {@code this} {@code Attachment} was last updated.
     *
     * @param dateLastUpdatedParam Attachment Last Updated Date.
     */
    public void setDateLastUpdated(Date dateLastUpdatedParam) {
        this.dateLastUpdated = dateLastUpdatedParam;
    }

    /**
     * Gets the {@code Date} when {@code this} {@code Attachment} was created.
     *
     * @return Attachment Created Date.
     */
    public Date getDateCreated() {
        return this.dateCreated;
    }

    /**
     * Sets the {@code Date} when {@code this} {@code Attachment} was created.
     *
     * @param dateCreatedParam Attachment Created Date.
     */
    public void setDateCreated(Date dateCreatedParam) {
        this.dateCreated = dateCreatedParam;
    }

    /**
     *

     * The data for {@code this} {@code Attachment} in Base-64 encoding.
     *
     *

     * Find out more at;
     * https://en.wikipedia.org/wiki/Base64
     *
     * @return Attachment Data in Base-64 encoding.
     */
    public String getAttachmentDataBase64() {
        return this.attachmentDataBase64;
    }

    /**
     *

     * The data for {@code this} {@code Attachment} in Base-64 encoding.
     *
     *

     * Find out more at;
     * https://en.wikipedia.org/wiki/Base64
     *
     * @param attachmentDataBase64Param Attachment Data in Base-64 encoding.
     */
    public void setAttachmentDataBase64(String attachmentDataBase64Param) {
        this.attachmentDataBase64 = attachmentDataBase64Param;
    }

    /**
     * Checks to see whether {@code this} {@code Attachment} name contains
     * the value {@code containingTextParam}.
     *
     * @param containingTextParam The text to check for (not case sensitive).
     * @return Whether the {@code Attachment} name contains {@code containingTextParam}.
     */
    @XmlTransient
    public boolean doesNameContain(String containingTextParam) {
        if (this.getName() == null || this.getName().trim().isEmpty()) {
            return false;
        }

        if (containingTextParam == null || containingTextParam.trim().isEmpty()) {
            return false;
        }

        String paramLower = containingTextParam.toLowerCase();

        String nameLower = this.getName().toLowerCase();

        return nameLower.contains(paramLower);
    }

    /**
     * Conversion to {@code JSONObject} from Java Object.
     *
     * @return {@code JSONObject} representation of {@code Attachment}
     * @throws JSONException If there is a problem with the JSON Body.
     *
     * @see ABaseFluidJSONObject#toJsonObject()
     */
    @Override
    public JSONObject toJsonObject() throws JSONException {

        JSONObject returnVal = super.toJsonObject();

        //Attachment Data...
        if (this.getAttachmentDataBase64() != null) {
            returnVal.put(JSONMapping.ATTACHMENT_DATA_BASE64,
                    this.getAttachmentDataBase64());
        }

        //Content Type...
        if (this.getContentType() != null) {
            returnVal.put(JSONMapping.CONTENT_TYPE, this.getContentType());
        }

        //Form Id...
        if (this.getFormId() != null) {
            returnVal.put(JSONMapping.FORM_ID, this.getFormId());
        }

        //Name...
        if (this.getName() != null) {
            returnVal.put(JSONMapping.NAME, this.getName());
        }

        //Path...
        if (this.getPath() != null) {
            returnVal.put(JSONMapping.PATH, this.getPath());
        }

        //Version...
        if (this.getVersion() != null) {
            returnVal.put(JSONMapping.VERSION, this.getVersion());
        }

        //Date Created...
        if (this.getDateCreated() != null) {
            returnVal.put(JSONMapping.DATE_CREATED,
                    this.getDateCreated().getTime());
        }

        //Date Last Updated...
        if (this.getDateLastUpdated() != null) {
            returnVal.put(JSONMapping.DATE_LAST_UPDATED,
                    this.getDateLastUpdated().getTime());
        }

        return returnVal;
    }
}


/*
 * Koekiebox CONFIDENTIAL
 *
 * [2012] - [2017] Koekiebox (Pty) Ltd
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property
 * of Koekiebox and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Koekiebox
 * and its suppliers and may be covered by South African and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material is strictly
 * forbidden unless prior written permission is obtained from Koekiebox.
 */

package com.fluidbpm.ws.client.v1.sqlutil;

import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import com.fluidbpm.program.api.vo.field.Field;
import com.fluidbpm.program.api.vo.form.Form;
import com.fluidbpm.program.api.vo.form.FormFieldListing;
import com.fluidbpm.program.api.vo.form.FormListing;
import com.fluidbpm.program.api.vo.item.FluidItem;
import com.fluidbpm.program.api.vo.ws.WS;
import com.fluidbpm.ws.client.FluidClientException;
import com.fluidbpm.ws.client.v1.ABaseClientWS;

/**
 * Java Web Service Client for {@code SQLUtil} related actions.
 *
 * @author jasonbruwer
 * @since v1.0
 *
 * @see JSONObject
 * @see WS.Path.FlowItem
 * @see FluidItem
 */
public class SQLUtilClient extends ABaseClientWS {

	/**
	 * Constructor that sets the Service Ticket from authentication.
	 *
	 * @param endpointBaseUrlParam URL to base endpoint.
	 * @param serviceTicketParam The Server issued Service Ticket.
	 */
	public SQLUtilClient(String endpointBaseUrlParam, String serviceTicketParam) {
		super(endpointBaseUrlParam);

		this.setServiceTicket(serviceTicketParam);
	}

	/**
	 * Retrieves all the Table Records (Forms) for the {@code formToGetTableFormsForParam}.
	 *
	 * @param formToGetTableFormsForParam The Fluid Form to get Table Fields for.
	 * @param includeFieldDataParam Should Table Record (Form) Field data be included?
	 *
	 * @return The {@code formToGetTableFormsForParam} Table Records as {@code Form}'s.
	 */
	public List<Form> getTableForms(
			Form formToGetTableFormsForParam,
			boolean includeFieldDataParam) {

		if (formToGetTableFormsForParam != null && this.serviceTicket != null) {
			formToGetTableFormsForParam.setServiceTicket(this.serviceTicket);
		}

		try {

			FormListing formListing = new FormListing(
					this.postJson(formToGetTableFormsForParam,
							WS.Path.SQLUtil.Version1.getTableForms(
									includeFieldDataParam)));

			return formListing.getListing();
		}
		//
		catch (JSONException e) {
			throw new FluidClientException(e.getMessage(), e,
					FluidClientException.ErrorCode.JSON_PARSING);
		}
	}

	/**
	 * Retrieves all Descendants for the {@code formToGetTableFormsForParam}.
	 *
	 * @param formToGetDescendantsForParam The Fluid Form to get Descendants for.
	 * @param includeFieldDataParam Should Descendant (Form) Field data be included?
	 * @param includeTableFieldsParam Should Table Record (Form) Field data be included?
	 * @param inclTableFieldFormInfoParam Include table record field info.
	 *
	 * @return The {@code formToGetTableFormsForParam} Descendants as {@code Form}'s.
	 */
	public List<Form> getDescendants(
			Form formToGetDescendantsForParam,
			boolean includeFieldDataParam,
			boolean includeTableFieldsParam,
			boolean inclTableFieldFormInfoParam) {

		if (formToGetDescendantsForParam != null && this.serviceTicket != null) {
			formToGetDescendantsForParam.setServiceTicket(this.serviceTicket);
		}

		try {
			FormListing formListing = new FormListing(
					this.postJson(formToGetDescendantsForParam,
							WS.Path.SQLUtil.Version1.getDescendants(
									includeFieldDataParam,
									includeTableFieldsParam,
									inclTableFieldFormInfoParam)));

			return formListing.getListing();
		}
		//
		catch (JSONException e) {
			throw new FluidClientException(e.getMessage(), e,
					FluidClientException.ErrorCode.JSON_PARSING);
		}
	}

	/**
	 * Retrieves the Ancestor for the {@code formToGetAncestorForParam}.
	 *
	 * @param formToGetAncestorForParam The Fluid Form to get Ancestor for.
	 * @param includeFieldDataParam Should Ancestor (Form) Field data be included?
	 * @param includeTableFieldsParam Should Table Record (Form) Field data be included?
	 *
	 * @return The {@code formToGetAncestorForParam} Ancestor as {@code Form}'s.
	 */
	public Form getAncestor(
			Form formToGetAncestorForParam,
			boolean includeFieldDataParam,
			boolean includeTableFieldsParam) {

		if (formToGetAncestorForParam != null && this.serviceTicket != null) {
			formToGetAncestorForParam.setServiceTicket(this.serviceTicket);
		}

		try {
			return new Form(
					this.postJson(formToGetAncestorForParam,
							WS.Path.SQLUtil.Version1.getAncestor(
									includeFieldDataParam,
									includeTableFieldsParam)));
		}
		//JSON Issue...
		catch (JSONException e) {
			throw new FluidClientException(e.getMessage(), e,
					FluidClientException.ErrorCode.JSON_PARSING);
		}
	}

	/**
	 * Retrieves all Fields for the {@code formToGetFieldsForParam}.
	 *
	 * @param formToGetFieldsForParam The Fluid Form to get Fields for.
	 * @param includeTableFieldsParam Should Table Field data be included?
	 *
	 * @return The {@code formToGetFieldsForParam} Fields as {@code Field}'s.
	 */
	public List<Field> getFormFields(
			Form formToGetFieldsForParam,
			boolean includeTableFieldsParam) {

		if (formToGetFieldsForParam != null && this.serviceTicket != null) {
			formToGetFieldsForParam.setServiceTicket(this.serviceTicket);
		}

		try {
			FormFieldListing formFieldListing = new FormFieldListing(
					this.postJson(formToGetFieldsForParam,
							WS.Path.SQLUtil.Version1.getFormFields(
									includeTableFieldsParam)));

			return formFieldListing.getListing();
		}
		//
		catch (JSONException e) {
			throw new FluidClientException(e.getMessage(), e,
					FluidClientException.ErrorCode.JSON_PARSING);
		}
	}
}


/*
 * Koekiebox CONFIDENTIAL
 *
 * [2012] - [2017] Koekiebox (Pty) Ltd
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property
 * of Koekiebox and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Koekiebox
 * and its suppliers and may be covered by South African and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material is strictly
 * forbidden unless prior written permission is obtained from Koekiebox.
 */

package com.fluidbpm.program.api.util;

import java.util.Base64;
import java.util.Date;

import javax.xml.bind.DatatypeConverter;

import org.json.JSONObject;

import com.fluidbpm.program.api.vo.field.Field;
import com.fluidbpm.program.api.vo.field.MultiChoice;
import com.fluidbpm.program.api.vo.field.TableField;

/**
 * Global utility class for the {@code com.fluidbpm.program.api.util} package.
 *
 * @author jasonbruwer on 2016/03/09.
 * @since 1.0
 */
public class UtilGlobal {

	/**
	 * Raygun API key for error tracking.
	 */
	public static final class Raygun {
		public static String API_KEY = null;
	}

	public static final String EMPTY = "";
	public static final String PIPE = "|";
	public static final String COMMA = ",";
	public static final String COMMA_SPACE = ", ";
	public static final String FORWARD_SLASH = "/";

	//RegEx...
	public static final String REG_EX_COMMA = "\\,";
	public static final String REG_EX_PIPE = "\\|";

	public static final String ZERO = "0";

	public static final String ENCODING_UTF_8 = "UTF-8";

	/**
	 * The field type id mappings.
	 */
	public static class FieldTypeId
	{
		public static final int _1_TEXT = 1;
		public static final int _2_TRUE_FALSE = 2;
		public static final int _3_PARAGRAPH_TEXT = 3;
		public static final int _4_MULTI_CHOICE = 4;
		public static final int _5_DATE_TIME = 5;
		public static final int _6_DECIMAL = 6;
		public static final int _7_TABLE_FIELD = 7;
		public static final int _8_TEXT_ENCRYPTED = 8;
		public static final int _9_LABEL = 9;
	}

	/**
	 * Utility method for creating camel-case-upper from {@code inputParam}
	 *
	 * @param inputParam The input to convert.
	 * @return The converted value.
	 */
	public String toCamelUpperCase(String inputParam) {

		if(inputParam == null)
		{
			return null;
		}

		if(inputParam.isEmpty())
		{
			return EMPTY;
		}

		char[] original = inputParam.toCharArray();

		StringBuilder titleCase =
				new StringBuilder(Character.toString(
						Character.toLowerCase(original[0])));

		boolean nextTitleCase = false;
		for(int index = 1;index < original.length;index++)
		{
			char c = original[index];

			if (Character.isSpaceChar(c)) {
				nextTitleCase = true;
				continue;
			}
			//Just add...
			else if (nextTitleCase) {
				c = Character.toTitleCase(c);
				nextTitleCase = false;
			}

			titleCase.append(c);
		}

		return titleCase.toString();
	}

	/**
	 * Extract and returns Latitude from {@code textToCheckParam} applicable to
	 * Fluid.
	 *
	 * @param textToCheckParam The Latitude and Longitude to extract Latitude from.
	 * @return Geo Latitude from {@code textToCheckParam}.
	 */
	public String getLatitudeFromFluidText(String textToCheckParam) {

		if(textToCheckParam == null || textToCheckParam.isEmpty()) {
			return EMPTY;
		}

		String[] latitudeAndLongitude = textToCheckParam.split(REG_EX_PIPE);
		if(latitudeAndLongitude == null || latitudeAndLongitude.length < 2) {
			latitudeAndLongitude = textToCheckParam.split(REG_EX_COMMA);
		}

		if(latitudeAndLongitude == null || latitudeAndLongitude.length == 0) {
			return ZERO;
		}

		if(latitudeAndLongitude.length > 1)
		{
			return toGoeSafe(latitudeAndLongitude[0]);
		}

		return ZERO;
	}

	/**
	 * Extract and returns Longitude from {@code textToCheckParam}
	 * applicable to ElasticSearch.
	 *
	 * @param textToCheckParam The Latitude and Longitude to extract Longitude from.
	 * @return Geo Longitude from {@code textToCheckParam}.
	 */
	public String getLongitudeFromFluidText(String textToCheckParam) {
		if(textToCheckParam == null || textToCheckParam.trim().isEmpty())
		{
			return EMPTY;
		}

		String[] latitudeAndLongitude = textToCheckParam.split(REG_EX_PIPE);
		if(latitudeAndLongitude == null || latitudeAndLongitude.length < 2) {
			latitudeAndLongitude = textToCheckParam.split(REG_EX_COMMA);
		}

		if(latitudeAndLongitude == null || latitudeAndLongitude.length == 0) {
			return ZERO;
		}

		if(latitudeAndLongitude.length > 1) {
			return this.toGoeSafe(latitudeAndLongitude[1]);
		}

		return ZERO;
	}

	/**
	 * Extract and returns Latitude from {@code textToCheckParam}
	 * applicable to ElasticSearch.
	 *
	 * @param textToCheckParam The Latitude and Longitude to extract Latitude from.
	 * @return Geo Latitude from {@code textToCheckParam}.
	 */
	public double getLatitudeFromElasticSearchText(String textToCheckParam)
	{
		if(textToCheckParam == null || textToCheckParam.isEmpty()) {
			return 0.0;
		}

		String[] latitudeAndLongitude = textToCheckParam.split(REG_EX_COMMA);
		if(latitudeAndLongitude == null || latitudeAndLongitude.length < 2) {
			latitudeAndLongitude = textToCheckParam.split(REG_EX_PIPE);
		}

		if(latitudeAndLongitude == null || latitudeAndLongitude.length == 0) {
			return 0.0;
		}

		if(latitudeAndLongitude.length > 1) {
			return toDoubleSafe(latitudeAndLongitude[0]);
		}

		return 0.0;
	}

	/**
	 * Extract and returns Longitude from {@code textToCheckParam}
	 * applicable to ElasticSearch.
	 *
	 * @param textToCheckParam The Latitude and Longitude to extract Longitude from.
	 * @return Geo Longitude from {@code textToCheckParam}.
	 */
	public double getLongitudeFromElasticSearchText(String textToCheckParam)
	{
		if(textToCheckParam == null || textToCheckParam.trim().isEmpty()) {
			return 0.0;
		}

		String[] latitudeAndLongitude = textToCheckParam.split(REG_EX_COMMA);
		if(latitudeAndLongitude == null || latitudeAndLongitude.length < 2) {
			latitudeAndLongitude = textToCheckParam.split(REG_EX_PIPE);
		}

		if(latitudeAndLongitude == null || latitudeAndLongitude.length == 0) {
			return 0.0;
		}

		if(latitudeAndLongitude.length > 1) {
			return this.toDoubleSafe(latitudeAndLongitude[1]);
		}

		return 0.0;
	}

	/**
	 * Convert the {@code toParseParam} to a double.
	 *
	 * @param toParseParam The {@code String} value to convert to {@code double}.
	 *
	 * @return {@code toParseParam} converted to a {@code double}.
	 */
	public final double toDoubleSafe(String toParseParam) {
		if (toParseParam == null || toParseParam.trim().isEmpty()) {
			return 0D;
		}
		try {
			return Double.parseDouble(toParseParam);
		} catch (NumberFormatException e) {
			return 0D;
		}
	}

	/**
	 * Convert the {@code toParseParam} to a double.
	 *
	 * @param toParseParam The {@code String} value to convert to {@code double}.
	 *
	 * @return {@code toParseParam} converted to a {@code double}.
	 */
	public final String toGoeSafe(String toParseParam) {
		if (toParseParam == null || toParseParam.trim().isEmpty()) {
			return ZERO;
		}
		try {
			for(char charToCheck : toParseParam.toCharArray()){

				if(!Character.isDigit(charToCheck) && '.' != charToCheck){
					return ZERO;
				}
			}

			if(toParseParam.length() > 12){
				return toParseParam.substring(0,12);
			}

			return toParseParam;
		} catch (NumberFormatException e) {
			return ZERO;
		}
	}

	/**
	 * Decodes the {@code base64StringParam} as {@code byte[]}.
	 *
	 * @param base64StringParam String to convert to {@code byte[]}.
	 * @return byte[].
	 */
	public static byte[] decodeBase64(String base64StringParam)
	{
		if(base64StringParam == null)
		{
			return null;
		}

		if(base64StringParam.isEmpty())
		{
			return new byte[]{};
		}

		return Base64.getDecoder().decode(base64StringParam);
	}

	/**
	 * Encodes the {@code bytesParam} as {@code java.lang.String}.
	 *
	 * @param bytesParam bytes to convert to Hex.
	 * @return Hex String in upper case.
	 */
	public static String encodeBase16(byte[] bytesParam)
	{
		if(bytesParam == null)
		{
			return null;
		}

		if(bytesParam.length == 0)
		{
			return UtilGlobal.EMPTY;
		}

		return DatatypeConverter.printHexBinary(bytesParam).toUpperCase();
	}

	/**
	 * Encodes the {@code stringParam} as {@code byte[]}.
	 *
	 * @param stringParam to convert to bytes.
	 * @return bytes from {@code stringParam}.
	 */
	public static byte[] decodeBase16(String stringParam) {

		if(stringParam == null)
		{
			return null;
		}

		if(stringParam.trim().isEmpty())
		{
			return new byte[]{};
		}

		int len = stringParam.length();
		byte[] data = new byte[len / 2];
		for (int i = 0; i < len; i += 2) {
			data[i / 2] = (byte) ((Character.digit(stringParam.charAt(i), 16) << 4)
					+ Character.digit(stringParam.charAt(i+1), 16));
		}

		return data;
	}

	/**
	 * Encodes the {@code bytesParam} as {@code java.lang.String}.
	 *
	 * @param bytesParam bytes to convert to Base64.
	 * @return Base64 String.
	 */
	public static String encodeBase64(byte[] bytesParam)
	{
		if(bytesParam == null)
		{
			return null;
		}

		if(bytesParam.length == 0)
		{
			return UtilGlobal.EMPTY;
		}

		return Base64.getEncoder().encodeToString(bytesParam);
	}

	/**
	 *
Adds all the elements of the given arrays into a new array.

	 *
The new array contains all of the element of {@code array1} followed
	 * by all of the elements {@code array2}. When an array is returned, it is always
	 * a new array.

	 *
	 *

	 * ArrayUtils.addAll(array1, null)   = cloned copy of array1
	 * ArrayUtils.addAll(null, array2)   = cloned copy of array2
	 * ArrayUtils.addAll([], [])         = []
	 *

	 *
	 * @param array1  the first array whose elements are added to the new array.
	 * @param array2  the second array whose elements are added to the new array.
	 * @return The new byte[] array.
	 * @since 2.1
	 */
	public static byte[] addAll(final byte[] array1, final byte... array2) {
		if (array1 == null) {
			return clone(array2);
		} else if (array2 == null) {
			return clone(array1);
		}
		final byte[] joinedArray = new byte[array1.length + array2.length];
		System.arraycopy(array1, 0, joinedArray, 0, array1.length);
		System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
		return joinedArray;
	}

	/**
	 *
Clones an array returning a typecast result and handling
	 * {@code null}.

	 *
	 *
This method returns {@code null} for a {@code null} input array.

	 *
	 * @param array  the array to clone, may be {@code null}
	 * @return the cloned array, {@code null} if {@code null} input
	 */
	public static byte[] clone(final byte[] array) {
		if (array == null) {
			return null;
		}
		return array.clone();
	}

	/**
	 * Sets the flat value on the {@code objectToSetFieldOnParam} JSON object.
	 *
	 * If the {@code fieldToExtractFromParam} is not provided, nothing will be set.
	 *
	 * If {@code fieldNamePrefixParam} or {@code fieldNameIdPrefixParam} is not set, an
	 * {@link NullPointerException} will be thrown.
	 *
	 * @param fieldNamePrefixParam The prefix field name.
	 * @param fieldNameIdPrefixParam The prefix field name for id.
	 * @param fieldToExtractFromParam The field to extract the value from.
	 * @param objectToSetFieldOnParam The JSON object to set the field value on.
	 *
	 * @see JSONObject
	 * @see Field
	 */
	public void setFlatFieldOnJSONObj(
			String fieldNamePrefixParam,
			String fieldNameIdPrefixParam,
			Field fieldToExtractFromParam,
			JSONObject objectToSetFieldOnParam)
	{
		if(fieldToExtractFromParam == null)
		{
			return;
		}

		String fieldName = fieldToExtractFromParam.getFieldNameAsUpperCamel();
		if(fieldName == null || fieldName.trim().isEmpty())
		{
			return;
		}

		String completeFieldName = fieldNamePrefixParam.concat(fieldName);
		String completeFieldNameId = fieldNameIdPrefixParam.concat(fieldName);

		objectToSetFieldOnParam.put(completeFieldNameId,
				fieldToExtractFromParam.getId());

		Object fieldValue = fieldToExtractFromParam.getFieldValue();

		if(fieldValue == null)
		{
			objectToSetFieldOnParam.put(
					completeFieldName,
					JSONObject.NULL);
		}
		//Table field...
		else if(fieldValue instanceof TableField)
		{
			return;
		}
		//Multiple Choice...
		else if(fieldValue instanceof MultiChoice) {

			MultiChoice multiChoice = (MultiChoice) fieldValue;

			//Nothing provided...
			if(multiChoice.getSelectedMultiChoices() == null ||
					multiChoice.getSelectedMultiChoices().isEmpty())
			{

				objectToSetFieldOnParam.put(
						completeFieldName,
						JSONObject.NULL);
				return;
			}

			StringBuilder builder = new StringBuilder();
			multiChoice.getSelectedMultiChoices().forEach(selectedChoice ->
			{
				builder.append(selectedChoice);
				builder.append(", ");
			});

			String selectVal = builder.toString();
			if(selectVal != null && !selectVal.trim().isEmpty())
			{
				selectVal = selectVal.substring(0, selectVal.length() - 2);
			}

			objectToSetFieldOnParam.put(completeFieldName, selectVal);
		}
		//Other valid types...
		else if((fieldValue instanceof Number || fieldValue instanceof Boolean) ||
				fieldValue instanceof String)
		{
			if((fieldValue instanceof String) &&
					Field.LATITUDE_AND_LONGITUDE.equals(
							fieldToExtractFromParam.getTypeMetaData()))
			{
				String formFieldValueStr = fieldValue.toString();

				String latitudeTxt = this.getLatitudeFromFluidText(formFieldValueStr);
				String longitudeTxt = this.getLongitudeFromFluidText(formFieldValueStr);

				fieldValue = (latitudeTxt.concat(UtilGlobal.COMMA).concat(longitudeTxt));
			}

			objectToSetFieldOnParam.put(completeFieldName, fieldValue);
		}
		//Date...
		else if(fieldValue instanceof Date)
		{
			objectToSetFieldOnParam.put(completeFieldName, ((Date)fieldValue).getTime());
		}
	}
}


package com.fluidbpm.ws.client.v1.websocket;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.json.JSONException;
import org.json.JSONObject;

import com.fluidbpm.program.api.util.UtilGlobal;
import com.fluidbpm.program.api.vo.ABaseFluidJSONObject;
import com.fluidbpm.program.api.vo.compress.CompressedResponse;
import com.fluidbpm.program.api.vo.ws.Error;
import com.fluidbpm.ws.client.FluidClientException;

/**
 * Base list message handler.
 *
 * @author jasonbruwer on 2016/03/11.
 * @since 1.1
 *
 * @param <T> The response object generic.
 *
 * @see CompletableFuture
 */
public abstract class AGenericListMessageHandler<T extends ABaseFluidJSONObject>
		implements IMessageResponseHandler {

	private final List<T> returnValue;
	private List<Error> errors;

	private IMessageReceivedCallback<T> messageReceivedCallback;
	private boolean isConnectionClosed;

	private Set<String> expectedEchoMessagesBeforeComplete;
	private CompletableFuture<List<T>> completableFuture;

	private boolean compressedResponse;

	public static Charset CHARSET = null;

	/**
	 * Constructor for SQLResultSet callbacks.
	 *
	 * @param messageReceivedCallbackParam The callback events.
	 * @param compressedResponseParam Compress the SQL Result in Base-64.
	 */
	public AGenericListMessageHandler(
			IMessageReceivedCallback<T> messageReceivedCallbackParam,
			boolean compressedResponseParam) {

		this(messageReceivedCallbackParam);
		this.compressedResponse = compressedResponseParam;
	}

	/**
	 * Message handler with callback.
	 *
	 * @param messageReceivedCallbackParam The message callback observer.
	 */
	public AGenericListMessageHandler(IMessageReceivedCallback<T> messageReceivedCallbackParam) {

		this.messageReceivedCallback = messageReceivedCallbackParam;
		this.returnValue = new CopyOnWriteArrayList();
		this.errors = new CopyOnWriteArrayList();
		this.expectedEchoMessagesBeforeComplete = new CopyOnWriteArraySet();
		this.isConnectionClosed = false;
	}

	/**
	 * Checks whether {@code this} message handler can process
	 * the message {@code messageParam}
	 *
	 * @param messageParam The message to check for qualification.
	 * @return The JSONObject.
	 *
	 * @see JSONObject
	 */
	public Object doesHandlerQualifyForProcessing(String messageParam){

		JSONObject jsonObject = null;
		try{
			jsonObject = new JSONObject(messageParam);
		}catch (JSONException jsonExcept){
			throw new FluidClientException(
					"Unable to parse ["+messageParam+"]. "+
							jsonExcept.getMessage(),
					jsonExcept, FluidClientException.ErrorCode.JSON_PARSING);
		}

		Error fluidError = new Error(jsonObject);
		if(fluidError.getErrorCode() > 0) {
			return fluidError;
		}

		String echo = fluidError.getEcho();

		//We can process the me
		if(this.expectedEchoMessagesBeforeComplete.contains(echo)){
			return jsonObject;
		}

		return null;
	}

	/**
	 * 'Handles' the message.
	 * If there was an error, the object will be Error
	 * If there was no error, the object will be JSONObject
	 *
	 * @param objectToProcess The message to handle in JSON format.
	 *
	 * @see Error
	 * @see JSONObject
	 */
	@Override
	public void handleMessage(Object objectToProcess) {

		//There is an error...
		if(objectToProcess instanceof Error) {

			Error fluidError = ((Error)objectToProcess);
			this.errors.add(fluidError);

			//Do a message callback...
			if(this.messageReceivedCallback != null)
			{
				this.messageReceivedCallback.errorMessageReceived(fluidError);
			}

			//If complete future is provided...
			if(this.completableFuture != null)
			{
				this.completableFuture.completeExceptionally(
						new FluidClientException(
								fluidError.getErrorMessage(),
								fluidError.getErrorCode()));
			}
		}
		//No Error...
		else {
			JSONObject jsonObject = (JSONObject)objectToProcess;

			//Uncompress the compressed response...
			if(this.compressedResponse){

				CompressedResponse compressedResponse = new CompressedResponse(jsonObject);

				byte[] compressedJsonList =
						UtilGlobal.decodeBase64(compressedResponse.getDataBase64());

				byte[] uncompressedJson = null;
				try {
					uncompressedJson = this.uncompress(compressedJsonList);
				} catch (IOException eParam) {
					throw new FluidClientException(
							"I/O issue with uncompress. "+eParam.getMessage(),
							eParam,
							FluidClientException.ErrorCode.IO_ERROR);
				}

				jsonObject = new JSONObject(new String(uncompressedJson));
			}

			T messageForm = this.getNewInstanceBy(jsonObject);

			//Add to the list of return values...
			this.returnValue.add(messageForm);

			//Completable future is set, and all response messages received...
			if(this.completableFuture != null)
			{
				String echo = messageForm.getEcho();
				if(echo != null && !echo.trim().isEmpty())
				{
					this.expectedEchoMessagesBeforeComplete.remove(echo);
				}

				//All expected messages received...
				if(this.expectedEchoMessagesBeforeComplete.isEmpty())
				{
					this.completableFuture.complete(this.returnValue);
				}
			}

			//Do a message callback...
			if(this.messageReceivedCallback != null)
			{
				this.messageReceivedCallback.messageReceived(messageForm);
			}
		}
	}

	/**
	 * Retrieve the stored instance of {@code CompletableFuture}.
	 * @return local instance of CompletableFuture.
	 *
	 * @see CompletableFuture
	 */
	public CompletableFuture<List<T>> getCF() {

		if(this.completableFuture == null){
			this.completableFuture = new CompletableFuture<>();
		}

		return this.completableFuture;
	}

	/**
	 * Event for when connection is closed.
	 */
	@Override
	public void connectionClosed() {
		this.isConnectionClosed = true;

		if(this.completableFuture != null)
		{
			//If there was no error...
			if(this.getErrors().isEmpty())
			{
				this.completableFuture.complete(this.returnValue);
			}
			//there was an error...
			else
			{
				Error firstFluidError = this.getErrors().get(0);

				this.completableFuture.completeExceptionally(new FluidClientException(
						firstFluidError.getErrorMessage(),
						firstFluidError.getErrorCode()));
			}
		}
	}

	/**
	 * Checks whether the connection to the server is closed.
	 *
	 * @return Whether connection is closed.
	 */
	public boolean isConnectionClosed() {
		return this.isConnectionClosed;
	}

	/**
	 * Checks to see whether there are error responses.
	 *
	 * @return Whether there is an error.
	 */
	public boolean hasErrorOccurred()
	{
		return !this.errors.isEmpty();
	}

	/**
	 * Adds the {@code expectedMessageEchoParam} echo to expect as a
	 * return value before the Web Socket operation may be regarded as complete.
	 *
	 * @param expectedMessageEchoParam The echo to expect.
	 */
	public void addExpectedMessage(String expectedMessageEchoParam)
	{
		if(expectedMessageEchoParam == null ||
				expectedMessageEchoParam.trim().isEmpty()) {
			return;
		}

		this.expectedEchoMessagesBeforeComplete.add(expectedMessageEchoParam);
	}

	/**
	 * Error listing.
	 *
	 * @return The error that occured.
	 */
	public List<Error> getErrors() {
		return this.errors;
	}

	/**
	 * Create a new instance of {@code T}.
	 *
	 * @param jsonObjectParam The {@code JSONObject} to create an object from.
	 * @return An instance of {@code ABaseFluidJSONObject}.
	 *
	 * @see ABaseFluidJSONObject
	 * @see JSONObject
	 */
	public abstract T getNewInstanceBy(JSONObject jsonObjectParam);

	/**
	 * Gets the size of the return value.
	 *
	 * @return Added Count.
	 */
	public int getAddedCount()
	{
		return this.returnValue.size();
	}

	/**
	 * Gets a list of echo messages of the current return values.
	 *
	 * @return The return value echo messages.
	 */
	private List<String> getEchoMessagesFromReturnValue()
	{
		List<String> returnListing = new ArrayList();

		if(this.returnValue == null)
		{
			return returnListing;
		}

		Iterator<T> iterForReturnVal =
				this.returnValue.iterator();

		//Only add where the ECHO message is set...
		while(iterForReturnVal.hasNext())
		{
			T returnVal = iterForReturnVal.next();

			if(returnVal.getEcho() == null)
			{
				continue;
			}

			returnListing.add(returnVal.getEcho());
		}

		return returnListing;
	}

	/**
	 * Checks the local return value echo messages if all
	 * of them contain {@code echoMessageParam}.
	 *
	 * @param echoMessageParam The echo messages to check.
	 *
	 * @return Whether local return value echo messages contain {@code echoMessageParam}.
	 */
	public boolean doReturnValueEchoMessageContainAll(
			List<String> echoMessageParam)
	{
		if(echoMessageParam == null || echoMessageParam.isEmpty())
		{
			return false;
		}

		List<String> allReturnValueEchoMessages =
				this.getEchoMessagesFromReturnValue();

		for(String toCheckFor: echoMessageParam)
		{
			if(!allReturnValueEchoMessages.contains(toCheckFor))
			{
				return false;
			}
		}

		return true;
	}

	/**
	 * Clears the return value.
	 */
	public void clear()
	{
		this.returnValue.clear();
	}

	/**
	 * Gets the return value.
	 *
	 * @return The return value listing.
	 */
	public List<T> getReturnValue() {
		return this.returnValue;
	}

	/**
	 * Uncompress the raw {@code compressedBytesParam}.
	 *
	 * @param compressedBytesParam The compressed bytes to uncompress.
	 *
	 * @return Uncompressed bytes.
	 *
	 * @throws IOException - If there is an issue during the un-compression.
	 */
	protected byte[] uncompress(
			byte[] compressedBytesParam
	) throws IOException {

		byte[] buffer = new byte[1024];

		byte[] returnVal = null;
		ZipInputStream zis = null;
		if(CHARSET == null) {
			zis = new ZipInputStream(
					new ByteArrayInputStream(compressedBytesParam));
		} else {
			zis = new ZipInputStream(
					new ByteArrayInputStream(compressedBytesParam),
					CHARSET);
		}

		//get the zip file content
		ByteArrayOutputStream bos = new ByteArrayOutputStream();

		//get the zipped file list entry
		ZipEntry ze = zis.getNextEntry();
		if(ze == null){
			return returnVal;
		}

		int len;
		while ((len = zis.read(buffer)) > 0) {
			bos.write(buffer, 0, len);
		}

		zis.closeEntry();
		zis.close();

		bos.flush();
		bos.close();

		returnVal = bos.toByteArray();

		return returnVal;
	}
}


/*
 * Koekiebox CONFIDENTIAL
 *
 * [2012] - [2017] Koekiebox (Pty) Ltd
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property
 * of Koekiebox and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Koekiebox
 * and its suppliers and may be covered by South African and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material is strictly
 * forbidden unless prior written permission is obtained from Koekiebox.
 */

package com.fluidbpm.ws.client.v1.user;

import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import com.fluidbpm.program.api.vo.form.Form;
import com.fluidbpm.program.api.vo.item.FluidItem;
import com.fluidbpm.program.api.vo.item.FluidItemListing;
import com.fluidbpm.program.api.vo.user.User;
import com.fluidbpm.program.api.vo.ws.WS;
import com.fluidbpm.ws.client.FluidClientException;
import com.fluidbpm.ws.client.v1.ABaseClientWS;

/**
 * Java Web Service Client for Personal inventory related actions.
 *
 * @author jasonbruwer
 * @since v1.4 2017-02-13
 *
 * @see JSONObject
 * @see WS.Path.FlowItem
 * @see FluidItem
 */
public class PersonalInventoryClient extends ABaseClientWS {

    /**
     * Constructor that sets the Service Ticket from authentication.
     *
     * @param endpointBaseUrlParam URL to base endpoint.
     * @param serviceTicketParam The Server issued Service Ticket.
     */
    public PersonalInventoryClient(
            String endpointBaseUrlParam,
            String serviceTicketParam) {
        super(endpointBaseUrlParam);

        this.setServiceTicket(serviceTicketParam);
    }

    /**
     * Retrieves all Personal Inventory items for the logged in user.
     *
     * @return The Personal Inventory items for the logged in {@code User}.
     */
    public List<FluidItem> getPersonalInventoryItems()
    {
        User loggedInUser = new User();

        if(this.serviceTicket != null)
        {
            loggedInUser.setServiceTicket(this.serviceTicket);
        }

        try {
            return new FluidItemListing(this.postJson(
                    loggedInUser,
                    WS.Path.PersonalInventory.Version1.getAllByLoggedInUser())).getListing();
        }
        //rethrow as a Fluid Client exception.
        catch (JSONException jsonExcept) {
            throw new FluidClientException(jsonExcept.getMessage(),
                    FluidClientException.ErrorCode.JSON_PARSING);
        }
    }

    /**
     * Remove the item {@code formToRemoveParam} from the personal inventory.
     *
     * @param formToRemoveParam The {@code Form} to remove from personal inventory.
     *
     * @return The Personal Inventory items removed for {@code User}.
     */
    public Form removeFromPersonalInventory(Form formToRemoveParam)
    {
        if(formToRemoveParam != null &&
                this.serviceTicket != null)
        {
            formToRemoveParam.setServiceTicket(this.serviceTicket);
        }

        try {
            return new Form(this.postJson(
                    formToRemoveParam,
                    WS.Path.PersonalInventory.Version1.removeFromPersonalInventory()));
        }
        //rethrow as a Fluid Client exception.
        catch (JSONException jsonExcept) {
            throw new FluidClientException(jsonExcept.getMessage(),
                    FluidClientException.ErrorCode.JSON_PARSING);
        }
    }

    /**
     * Clears all Personal Inventory items for the logged in user.
     *
     * @return The cleared Personal Inventory items for the logged in {@code User}.
     */
    public List<FluidItem> clearPersonalInventoryItems()
    {
        User loggedInUser = new User();

        if(this.serviceTicket != null)
        {
            loggedInUser.setServiceTicket(this.serviceTicket);
        }

        try {
            return new FluidItemListing(this.postJson(
                    loggedInUser,
                    WS.Path.PersonalInventory.Version1.clearPersonalInventory())).getListing();
        }
        //rethrow as a Fluid Client exception.
        catch (JSONException jsonExcept) {
            throw new FluidClientException(jsonExcept.getMessage(),
                    FluidClientException.ErrorCode.JSON_PARSING);
        }
    }
}


/*
 * Koekiebox CONFIDENTIAL
 *
 * [2012] - [2017] Koekiebox (Pty) Ltd
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property
 * of Koekiebox and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Koekiebox
 * and its suppliers and may be covered by South African and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material is strictly
 * forbidden unless prior written permission is obtained from Koekiebox.
 */

package com.fluidbpm.program.api.vo.ws.auth;

import org.json.JSONException;
import org.json.JSONObject;

import com.fluidbpm.program.api.vo.ABaseFluidJSONObject;

/**
 * Authorization request token data.
 *
 * @author jasonbruwer
 * @since v1.0
 *
 * @see ABaseFluidJSONObject
 * @see TokenStatus
 * @see AuthRequest
 * @see AuthResponse
 * @see AuthEncryptedData
 */
public class AppRequestToken extends ABaseFluidJSONObject {

    public static final long serialVersionUID = 1L;

    //Payload...
    private String encryptedDataBase64;//Contains [TicketExpires], [Roles] and [SessionKey]...
    private String encryptedDataHmacBase64;
    private String ivBase64;
    private String seedBase64;

    //
    private String principalClient;
    private String salt;
    private String roleString;
    private Long timestamp;

    /**
     * The JSON mapping for the {@code AppRequestToken} object.
     */
    public static class JSONMapping {
        public static final String ENCRYPTED_DATA_BASE_64 = "encryptedDataBase64";
        public static final String ENCRYPTED_DATA_HMAC_BASE_64 = "encryptedDataHmacBase64";
        public static final String IV_BASE_64 = "ivBase64";
        public static final String SEED_BASE_64 = "seedBase64";

        public static final String SALT = "salt";
        public static final String PRINCIPAL_CLIENT = "principalClient";
        public static final String ROLE_STRING = "roleString";
        public static final String TIMESTAMP = "timestamp";
    }

    /**
     * Default constructor.
     */
    public AppRequestToken() {
        super();
    }

    /**
     * Populates local variables with {@code jsonObjectParam}.
     *
     * @param jsonObjectParam The JSON Object.
     */
    public AppRequestToken(JSONObject jsonObjectParam){
        super(jsonObjectParam);

        //Encrypted Data Base64
        if (!this.jsonObject.isNull(JSONMapping.ENCRYPTED_DATA_BASE_64)) {
            this.setEncryptedDataBase64(this.jsonObject.getString(JSONMapping.ENCRYPTED_DATA_BASE_64));
        }

        //Encrypted Data HMAC Base64
        if (!this.jsonObject.isNull(JSONMapping.ENCRYPTED_DATA_HMAC_BASE_64)) {
            this.setEncryptedDataHmacBase64(this.jsonObject.getString(JSONMapping.ENCRYPTED_DATA_HMAC_BASE_64));
        }

        //IV Base 64...
        if (!this.jsonObject.isNull(JSONMapping.IV_BASE_64)) {
            this.setIvBase64(this.jsonObject.getString(JSONMapping.IV_BASE_64));
        }

        //Seed Base 64...
        if (!this.jsonObject.isNull(JSONMapping.SEED_BASE_64)) {
            this.setSeedBase64(this.jsonObject.getString(JSONMapping.SEED_BASE_64));
        }

        //Service Ticket 64...
        if (!this.jsonObject.isNull(ABaseFluidJSONObject.JSONMapping.SERVICE_TICKET)) {
            this.setServiceTicket(this.jsonObject.getString(ABaseFluidJSONObject.JSONMapping.SERVICE_TICKET));
        }

        //Salt...
        if (!this.jsonObject.isNull(JSONMapping.SALT)) {
            this.setSalt(this.jsonObject.getString(JSONMapping.SALT));
        }

        //Principal Client...
        if (!this.jsonObject.isNull(JSONMapping.PRINCIPAL_CLIENT)) {
            this.setPrincipalClient(this.jsonObject.getString(JSONMapping.PRINCIPAL_CLIENT));
        }

        //Role String...
        if (!this.jsonObject.isNull(JSONMapping.ROLE_STRING)) {
            this.setRoleString(this.jsonObject.getString(JSONMapping.ROLE_STRING));
        }

        //Timestamp...
        if (!this.jsonObject.isNull(JSONMapping.TIMESTAMP)) {
            this.setTimestamp(this.jsonObject.getLong(JSONMapping.TIMESTAMP));
        }
    }

    /**
     * Gets the Principal Client.
     *
     * @return Principal Client.
     */
    public String getPrincipalClient() {
        return this.principalClient;
    }

    /**
     * Sets the Principal Client.
     *
     * @param principalClient Principal Client.
     */
    public void setPrincipalClient(String principalClient) {
        this.principalClient = principalClient;
    }

    /**
     * Gets the Timestamp.
     *
     * @return The timestamp milliseconds since January 1, 1970, 00:00:00 GMT
     */
    public Long getTimestamp() {
        return this.timestamp;
    }

    /**
     * Sets the Timestamp.
     *
     * @param timestamp The timestamp milliseconds since January 1, 1970, 00:00:00 GMT
     */
    public void setTimestamp(Long timestamp) {
        this.timestamp = timestamp;
    }

    /**
     * Gets the EncryptedData in Base64.
     *
     * @return EncryptedData in Base64.
     */
    public String getEncryptedDataBase64() {
        return this.encryptedDataBase64;
    }

    /**
     * Sets the EncryptedData in Base64.
     *
     * @param encryptedDataBase64 EncryptedData in Base64.
     */
    public void setEncryptedDataBase64(String encryptedDataBase64) {
        this.encryptedDataBase64 = encryptedDataBase64;
    }

    /**
     * Gets the EncryptedData for HMAC in Base64.
     *
     * @return Encrypted Data in Hmac Base64.
     */
    public String getEncryptedDataHmacBase64() {
        return this.encryptedDataHmacBase64;
    }

    /**
     * Sets the EncryptedData for HMAC in Base64.
     *
     * @param encryptedDataHmacBase64 Encrypted Data in Hmac Base64.
     */
    public void setEncryptedDataHmacBase64(String encryptedDataHmacBase64) {
        this.encryptedDataHmacBase64 = encryptedDataHmacBase64;
    }

    /**
     * Gets the Initialization Vector in Base-64.
     *
     * @return Initialization Vector in Base-64 format.
     */
    public String getIvBase64() {
        return this.ivBase64;
    }

    /**
     * Sets the Initialization Vector in Base-64.
     *
     * @param ivBase64Param Initialization Vector in Base-64 format.
     */
    public void setIvBase64(String ivBase64Param) {
        this.ivBase64 = ivBase64Param;
    }

    /**
     * Gets the SEED in Base-64 format.
     *
     * @return SEED in Base-64 format.
     */
    public String getSeedBase64() {
        return this.seedBase64;
    }

    /**
     * Sets the SEED in Base-64 format.
     *
     * @param seedBase64 SEED in Base-64 format.
     */
    public void setSeedBase64(String seedBase64) {
        this.seedBase64 = seedBase64;
    }

    /**
     * Gets 20 Random ASCII {@code Character}s
     *
     * @return 20 Random ASCII {@code Character}s paired with the {@code User} password.
     */
    public String getSalt() {
        return this.salt;
    }

    /**
     * Sets 20 Random ASCII {@code Character}s
     *
     * @param saltParam 20 Random ASCII {@code Character}s paired with the {@code User} password.
     */
    public void setSalt(String saltParam) {
        this.salt = saltParam;
    }

    /**
     * Gets the role listing separated by ','.
     *
     * @return List of roles.
     */
    public String getRoleString() {
        return this.roleString;
    }

    /**
     * Sets the role listing separated by ','.
     *
     * @param roleStringParam List of roles.
     */
    public void setRoleString(String roleStringParam) {
        this.roleString = roleStringParam;
    }

    /**
     * Conversion to {@code JSONObject} from Java Object.
     *
     * @return {@code JSONObject} representation of {@code AppRequestToken}
     * @throws JSONException If there is a problem with the JSON Body.
     *
     * @see ABaseFluidJSONObject#toJsonObject()
     */
    @Override
    public JSONObject toJsonObject() throws JSONException {

        JSONObject returnVal = super.toJsonObject();

        //Encrypted Data Base 64...
        if (this.getEncryptedDataBase64() != null) {
            returnVal.put(JSONMapping.ENCRYPTED_DATA_BASE_64, this.getEncryptedDataBase64());
        }

        //Encrypted Data HMAC Base 64...
        if (this.getEncryptedDataHmacBase64() != null) {
            returnVal.put(JSONMapping.ENCRYPTED_DATA_HMAC_BASE_64, this.getEncryptedDataHmacBase64());
        }

        //IV Base 64...
        if (this.getIvBase64() != null) {
            returnVal.put(JSONMapping.IV_BASE_64, this.getIvBase64());
        }

        //Seed Base 64...
        if (this.getSeedBase64() != null) {
            returnVal.put(JSONMapping.SEED_BASE_64, this.getSeedBase64());
        }

        //Service Ticket Base 64...
        if (this.getServiceTicket() != null) {
            returnVal.put(ABaseFluidJSONObject.JSONMapping.SERVICE_TICKET, this.getServiceTicket());
        }

        //Salt...
        if (this.getSalt() != null) {
            returnVal.put(JSONMapping.SALT, this.getSalt());
        }

        //Principal Client...
        if (this.getPrincipalClient() != null) {
            returnVal.put(JSONMapping.PRINCIPAL_CLIENT, this.getPrincipalClient());
        }

        //Role String...
        if (this.getRoleString() != null) {
            returnVal.put(JSONMapping.ROLE_STRING, this.getRoleString());
        }

        //Timestamp...
        if (this.getTimestamp() != null) {
            returnVal.put(JSONMapping.TIMESTAMP, this.getTimestamp());
        }

        return returnVal;
    }
}


/*
 * Koekiebox CONFIDENTIAL
 *
 * [2012] - [2017] Koekiebox (Pty) Ltd
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property
 * of Koekiebox and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Koekiebox
 * and its suppliers and may be covered by South African and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material is strictly
 * forbidden unless prior written permission is obtained from Koekiebox.
 */

package com.fluidbpm.ws.client.v1.form;

import java.io.UnsupportedEncodingException;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import com.fluidbpm.program.api.vo.field.Field;
import com.fluidbpm.program.api.vo.flow.JobView;
import com.fluidbpm.program.api.vo.form.Form;
import com.fluidbpm.program.api.vo.form.TableRecord;
import com.fluidbpm.program.api.vo.historic.FormFlowHistoricData;
import com.fluidbpm.program.api.vo.historic.FormFlowHistoricDataListing;
import com.fluidbpm.program.api.vo.historic.FormHistoricData;
import com.fluidbpm.program.api.vo.historic.FormHistoricDataListing;
import com.fluidbpm.program.api.vo.user.User;
import com.fluidbpm.program.api.vo.ws.WS;
import com.fluidbpm.ws.client.FluidClientException;
import com.fluidbpm.ws.client.v1.ABaseClientWS;

/**
 * Java Web Service Client for Electronic Form related actions.
 *
 * @author jasonbruwer
 * @since v1.0
 *
 * @see JSONObject
 * @see com.fluidbpm.program.api.vo.ws.WS.Path.FormContainer
 * @see Form
 */
public class FormContainerClient extends ABaseClientWS {

    /**
     * Constructor that sets the Service Ticket from authentication.
     *
     * @param endpointBaseUrlParam URL to base endpoint.
     * @param serviceTicketParam The Server issued Service Ticket.
     */
    public FormContainerClient(
            String endpointBaseUrlParam,
            String serviceTicketParam) {
        super(endpointBaseUrlParam);

        this.setServiceTicket(serviceTicketParam);
    }

    /**
     * Create a new Form Container / Electronic Forms.
     *
     * @param formParam The Form to create.
     * @return Created Form Container / Electronic Form.
     *
     * @see Field
     */
    public Form createFormContainer(Form formParam) {
        if(formParam != null && this.serviceTicket != null) {
            formParam.setServiceTicket(this.serviceTicket);
        }

        return new Form(this.putJson(
                formParam, WS.Path.FormContainer.Version1.formContainerCreate()));
    }

    /**
     * Create a new Table Record.
     *
     * @param tableRecordParam The Table Record to create.
     * @return Created Table Record.
     *
     * @see TableRecord
     */
    public TableRecord createTableRecord(TableRecord tableRecordParam) {
        if(tableRecordParam != null && this.serviceTicket != null) {
            tableRecordParam.setServiceTicket(this.serviceTicket);
        }

        return new TableRecord(this.putJson(
                tableRecordParam,
                WS.Path.FormContainerTableRecord.Version1.formContainerTableRecordCreate()));
    }

    /**
     * Update a Form Container / Electronic Form.
     * The table record forms may also be updated with
     *
     * @param formParam The Form to update.
     * @return Updated Form Container / Electronic Form.
     *
     * @see Field
     * @see TableRecord
     * @see Form
     */
    public Form updateFormContainer(Form formParam) {
        if(formParam != null && this.serviceTicket != null) {
            formParam.setServiceTicket(this.serviceTicket);
        }

        return new Form(this.postJson(
                formParam, WS.Path.FormContainer.Version1.formContainerUpdate()));
    }

    /**
     * Execute the Custom Program with action alias {@code customWebActionParam}.
     * This method may be used for Form and Table Records.
     *
     * @param customWebActionParam The custom web action name. Action identifier.
     * @param formParam The Form to send for 3rd Party execution.
     *
     * @return Result after the 3rd Party Custom Web Action completed.
     *
     * @see Field
     * @see Form
     * @see com.fluidbpm.program.api.vo.thirdpartylib.ThirdPartyLibrary
     */
    public Form executeCustomWebAction(
            String customWebActionParam,
            Form formParam) {
        return this.executeCustomWebAction(
                customWebActionParam,
                false,
                null,
                formParam);
    }

    /**
     * Execute the Custom Program with action alias {@code customWebActionParam}.
     * This method may be used for Form and Table Records.
     *
     * @param customWebActionParam The custom web action name. Action identifier.
     * @param isTableRecordParam Is the form a table record form.
     * @param formContainerTableRecordBelowsToParam The parent form container if table record.
     * @param formParam The Form to send for 3rd Party execution.
     *
     * @return Result after the 3rd Party Custom Web Action completed.
     *
     * @see Field
     * @see Form
     * @see TableRecord
     * @see com.fluidbpm.program.api.vo.thirdpartylib.ThirdPartyLibrary
     */
    public Form executeCustomWebAction(
            String customWebActionParam,
            boolean isTableRecordParam,
            Long formContainerTableRecordBelowsToParam,
            Form formParam) {

        if(formParam != null && this.serviceTicket != null) {
            formParam.setServiceTicket(this.serviceTicket);
        }

        if(customWebActionParam == null || customWebActionParam.trim().isEmpty()){
            throw new FluidClientException(
                    "Custom Web Action is mandatory.",
                    FluidClientException.ErrorCode.FIELD_VALIDATE);
        }

        try {
            return new Form(this.postJson(
                    formParam, WS.Path.FormContainer.Version1.executeCustomWebAction(
                            customWebActionParam,
                            isTableRecordParam,
                            formContainerTableRecordBelowsToParam)));
        } catch (UnsupportedEncodingException unsEncExcept) {
            throw new FluidClientException(unsEncExcept.getMessage(),
                    unsEncExcept, FluidClientException.ErrorCode.IO_ERROR);
        }
    }

    /**
     * Deletes the Form Container provided.
     * Id must be set on the Form Container.
     *
     * @param formContainerParam The Form Container to Delete.
     * @return The deleted Form Container.
     */
    public Form deleteFormContainer(Form formContainerParam) {

        if(formContainerParam != null && this.serviceTicket != null) {
            formContainerParam.setServiceTicket(this.serviceTicket);
        }

        return new Form(this.postJson(formContainerParam,
                WS.Path.FormContainer.Version1.formContainerDelete()));
    }

    /**
     * Retrieves Electronic Form Workflow historic information.
     *
     * The Form Id must be provided.
     *
     * @param formParam The form to retrieve historic data for.
     * @return Electronic Form Workflow historic data.
     */
    public List<FormFlowHistoricData> getFormFlowHistoricData(Form formParam) {
        if(formParam != null && this.serviceTicket != null) {
            formParam.setServiceTicket(this.serviceTicket);
        }

        return new FormFlowHistoricDataListing(this.postJson(
                formParam, WS.Path.FlowItemHistory.Version1.getByFormContainer())).getListing();
    }

    /**
     * Retrieves Electronic Form and Field historic information.
     *
     * The Form Id must be provided.
     *
     * @param includeCurrentParam Whether the current values should be included.
     *
     * @param formParam The form to retrieve historic data for.
     * @return Electronic Form and Field historic data.
     */
    public List<FormHistoricData> getFormAndFieldHistoricData(
        Form formParam,
        boolean includeCurrentParam
    ) {
        if(formParam != null && this.serviceTicket != null)
        {
            formParam.setServiceTicket(this.serviceTicket);
        }

        return new FormHistoricDataListing(this.postJson(
                formParam, WS.Path.FormHistory.Version1.getByFormContainer(
                        includeCurrentParam))).getListing();
    }

    /**
     * Retrieves Electronic Form and Field historic information for
     * the most recent modification.
     *
     * The Form Id must be provided.
     *
     * @param formParam The form to retrieve historic data for.
     *
     * @return Electronic Form and Field historic data.
     */
    public FormHistoricData getMostRecentFormAndFieldHistoricData(Form formParam) {
        if(formParam != null && this.serviceTicket != null) {
            formParam.setServiceTicket(this.serviceTicket);
        }

        return new FormHistoricData(this.postJson(
                formParam, WS.Path.FormHistory.Version1.getByMostRecentByFormContainer()));
    }

    /**
     * Retrieves the Form Container by Primary key.
     *
     * @param formContainerIdParam The Form Container primary key.
     * @return Form by Primary key.
     */
    public Form getFormContainerById(Long formContainerIdParam) {
        Form form = new Form(formContainerIdParam);

        if(this.serviceTicket != null) {
            form.setServiceTicket(this.serviceTicket);
        }

        return new Form(this.postJson(
                form, WS.Path.FormContainer.Version1.getById()));
    }

    /**
     * Lock the provided form container for logged in user.
     *
     * @param formParam The form to lock.
     * @param jobViewParam If retrieved from a view, the lock to view from.
     *
     * @return The locked form.
     */
    public Form lockFormContainer(
            Form formParam,
            JobView jobViewParam
    ) {
        return this.lockFormContainer(
                formParam, jobViewParam, null);
    }

    /**
     * Lock the provided form container for logged in user.
     * If {@code userToLockAsParam} is provided and valid, that user will be used instead.
     *
     * @param formParam The form to lock.
     * @param jobViewParam If retrieved from a view, the lock to view from.
     * @param userToLockAsParam The form will be locked as this user.
     *                          The logged in user must have permission to perform this action.
     *
     * @return The locked form.
     */
    public Form lockFormContainer(
            Form formParam,
            JobView jobViewParam,
            User userToLockAsParam
    ) {
        if(this.serviceTicket != null && formParam != null) {
            formParam.setServiceTicket(this.serviceTicket);
        }

        Long jobViewId = (jobViewParam == null) ?
                null : jobViewParam.getId();

        Long lockAsUserId = (userToLockAsParam == null) ?
                null : userToLockAsParam.getId();

        try {
            return new Form(this.postJson(
                    formParam,
                    WS.Path.FormContainer.Version1.lockFormContainer(
                            jobViewId, lockAsUserId)));
        }
        //rethrow as a Fluid Client exception.
        catch (JSONException jsonExcept) {
            throw new FluidClientException(jsonExcept.getMessage(),
                    FluidClientException.ErrorCode.JSON_PARSING);
        }
    }

    /**
     * Unlock the provided form container from the logged in user.
     * Item will not be removed from users Personal Inventory.
     *
     * @param formParam The form to unlock.
     *
     * @return The un-locked form.
     */
    public Form unLockFormContainer(Form formParam) {
        return this.unLockFormContainer(
                formParam, null,true,false);
    }

    /**
     * Unlock the provided form container from the logged in user.
     * Item will not be removed from users Personal Inventory.
     *
     * @param formParam The form to unlock.
     * @param unlockAsyncParam Should the unlock be performed asynchronous.
     *
     * @return The un-locked form.
     */
    public Form unLockFormContainer(
            Form formParam,
            boolean unlockAsyncParam) {
        return this.unLockFormContainer(
                formParam, null, unlockAsyncParam, false);
    }

    /**
     * Unlock the provided form container from the logged in user.
     * The unlock will be performed asynchronous.
     * Item will not be removed from users Personal Inventory.
     *
     * @param formParam The form to unlock.
     * @param userToUnLockAsParam The form will be un-locked as this user.
     *                          The logged in user must have permission to perform this action.
     *
     * @return The un-locked form.
     */
    public Form unLockFormContainer(
            Form formParam,
            User userToUnLockAsParam) {
        return this.unLockFormContainer(
                formParam, userToUnLockAsParam, true, false);
    }

    /**
     * Unlock the provided form container from the logged in user.
     * The unlock will be performed asynchronous.
     * Item will not be removed from users Personal Inventory.
     *
     * @param formParam The form to unlock.
     * @param userToUnLockAsParam The form will be un-locked as this user.
     *                          The logged in user must have permission to perform this action.
     * @param unlockAsyncParam Should the unlock be performed asynchronous.
     *
     * @return The un-locked form.
     */
    public Form unLockFormContainer(
            Form formParam,
            User userToUnLockAsParam,
            boolean unlockAsyncParam) {
        return this.unLockFormContainer(
                formParam,
                userToUnLockAsParam,
                unlockAsyncParam,
                false);
    }

    /**
     * Unlock the provided form container from the logged in user.
     *
     * @param formParam The form to unlock.
     * @param userToUnLockAsParam The form will be un-locked as this user.
     *                          The logged in user must have permission to perform this action.
     * @param unlockAsyncParam Should the unlock be performed asynchronous.
     * @param removeFromPersonalInventoryParam Remove from Personal Inventory when unlocked.
     *
     * @return The un-locked form.
     */
    public Form unLockFormContainer(
            Form formParam,
            User userToUnLockAsParam,
            boolean unlockAsyncParam,
            boolean removeFromPersonalInventoryParam) {
        if(this.serviceTicket != null && formParam != null) {
            formParam.setServiceTicket(this.serviceTicket);
        }

        Long unLockAsUserId = (userToUnLockAsParam == null) ?
                null : userToUnLockAsParam.getId();

        try {
            return new Form(this.postJson(
                    formParam,
                    WS.Path.FormContainer.Version1.unLockFormContainer(
                            unLockAsUserId,
                            unlockAsyncParam,
                            removeFromPersonalInventoryParam)));
        }
        //rethrow as a Fluid Client exception.
        catch (JSONException jsonExcept) {
            throw new FluidClientException(jsonExcept.getMessage(),
                    FluidClientException.ErrorCode.JSON_PARSING);
        }
    }
}