/* Copyright Dassault Systemes, 1999, 2010 */


package examples.development.datatypes;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;

import com.engineous.common.i18n.IString;
import com.engineous.sdk.resmgr.ResMgr;
import com.engineous.sdk.runtime.RuntimeEnv;
import com.engineous.sdk.vars.AbstractDataHandler;
import com.engineous.sdk.vars.DataHandler;
import com.engineous.sdk.vars.FileUtil;
import com.engineous.sdk.vars.VarUnsupportedOperationException;
import com.engineous.sdk.vars.VariableException;
import com.engineous.sdk.vars.VariableUtil;

/**
 * File data handler for URLs.  This handler can be used for any
 * standard URL protocol (e.g. HTTP, FTP, etc).
 * <p>
 * Note: The following methods of the DataHandler interface are similar but subtly different.
 * Here are some notes to help in implementing these methods.
 * <ul>
 *   <li>getResourceName() - Return a single, readable string that defines the configuration of
 *       this handler, if there is such a string.  Return null if this handler cannot be configured
 *       by a single string.  This is a convenience method to configure the handler or edit the
 *       configuration without having to down-cast from DataHandler to a specific type.
 *   <li>getConfigDisplayString() - Return a user-readable string that quickly describes the configuration
 *       of this handler.  This does not need to be the full configuration the way getResourceName() must be,
 *       it only has to be enough to indicate to the user how the handler is configured.  All handlers must
 *       implement this and must not return null.
 *   <li>getOriginalFileName() - Returns the original file name used to configure the handler if there
 *       was one.  This name is used to guess a valid LOCAL file name that can be used to hold the data from
 *       this handler.  The default implementation in AbstractDataHandler takes the last part of whatever
 *       getResourceName() returns, unless a file name has been explicitly set by DataHandler.setOriginalFileName.
 *   <li>getFile() - Return a LOCAL file object that contains the data for this handler.  This is only really
 *       defined for a File Handler or something that behaves like the File handler.  Handlers that do not
 *       hold a reference to a local file should return null (which is the default implementation in
 *       AbstractDataHandler.
 * </ul>
 */
public class DataHandlerURL
		extends AbstractDataHandler {

	/**
	 * Incompatible changes to this class must update the following serialization
	 * ID (incrementing by 1 is sufficient).  Compatible changes should NOT change this value.
	 * Definition of incompatible change: http://java.sun.com/j2se/1.3/docs/guide/serialization/spec/version.doc7.html
	 * Definition of compatible change: http://java.sun.com/j2se/1.3/docs/guide/serialization/spec/version.doc8.html
	 */
	static final long serialVersionUID = 1L;

	// Used by the Isight Internationalization tool when translating messages.  Needed for IString constructor.
	transient private static final Class CLASS = DataHandlerURL.class;

	/** The URL this handler is configured to reference, as a string. */
	private String rawURLName = null;

	/** The URL after it has had {var xxx} substitutions resolved */
	private String resolvedURLName = null;

	/**
	 * The URL this handler references as a URL object.  Null until the URL is first needed.
	 * Is transient as it is re-created from the resolvedURLName as needed.
	 */
	private transient URL url = null;

	/**
	 * Constructs an unconfigured DataHandlerURL
	 */
	public DataHandlerURL() {}

	/**
	 * Get the resolved URL.  This class supports {var xxx} substitutions in the URL
	 * (it does not support {root xxx} substitutions).
	 * The resolved URL is saved as it may be needed in the RT Gateway to retrieve the contents of an output URL file.
	 * @return URL as a string, with all substitutions done.
	 * @throws VariableException
	 */
	protected String getURLName()
			throws VariableException {

		// If there is nothing to resolve, just return null.
		if (rawURLName == null) {
			return null;
		}

		// If the resolved name has not been set yet, set it by resolving substitutions.
		if (resolvedURLName == null) {
			try {

				// Substitute {var xxx} but not {root xxx}
				resolvedURLName = VariableUtil.substituteBasicSymbolics(rawURLName, (RuntimeEnv)null, false);

				// Inform any listeners that the configuration has changed.
				fireChangeEvent();
			}
			catch (Throwable t) {
				throw new VariableException(t, new IString(CLASS, 31858, "Unable to resolve substitutions in a URL file parameter."));
			}
		}

		return resolvedURLName;
	}

	/**
	 * Get the configuration in a form that can be displayed to the user.
	 * This is just the URL if set, else "[Undefined]".
	 * All handlers must implement this method in some way.  It is not necessary that the
	 * returned string be the full configuration of the handler, only that it give a user an
	 * idea what resource the handler points to.  For example, the FTP handler could return the
	 * server and file name here, while not returning the user name and password used to log
	 * in to the server.
	 * <p><i>Implements interface <b>DataHandler</b></i></p>
	 * @return displayable string describing the configuration of this handler.
	 */
	public String getConfigDisplayString() {

		if ((rawURLName != null) && (rawURLName.length() > 0)) {
			return rawURLName;
		}
		else {
			return ResMgr.getMessage(CLASS, 92123, "[Undefined]");
		}
	}

	/**
	 * Returns the a connection for the URL that is configured in this
	 * handler.  An VariableException is thrown if this handler is not configured
	 * or the connection fails.
	 * This is used internally as the first step toward getting an output stream to write the URL or
	 * an input stream to read the URL.
	 * @return URLConnection
	 * @throws IOException
	 * @throws VariableException
	 */
	protected URLConnection getConnection()
			throws VariableException, IOException {

		if (rawURLName == null) {
			throw new VariableException(new IString(CLASS, 78156, "URL data handler is not configured."));
		}

		url = new URL(getURLName());
		URLConnection connection = url.openConnection();
		connection.setDoInput(true);
		connection.setDoOutput(true);

		return connection;
	}

	//=========================================================================
	/**
	 * Returns an output stream on the configured URL.
	 * This is used when post-processing an output file parameter, to copy the file from
	 * the runtime directory to the Handler.
	 * Most URL handlers don't support output, so this rarely works.
	 * Only ftp: and file: handlers are likely to support output.
	 * Some HTTP servers support the PUT operation to allow users to write files to the server.
	 * This is different from the POST operation used to upload a file to a web form.
	 * <p><i>Implements interface <b>DataHandler</b></i></p>
	 * @return OutputStream
	 * @throws VariableException
	 */
	public OutputStream getOutputStream()
			throws VariableException {

		try {

			URLConnection connection = getConnection();
			if (!(connection instanceof HttpURLConnection)) {
				// Non-http connections just use their output stream
				return connection.getOutputStream();
			}
			else {
				// Http-connections do output via a PUT request.
				// HttpURLConnection doesn't actually transmit the data until
				// getInputStream or getResponseCode is called.
				final HttpURLConnection httpConnection = (HttpURLConnection)connection;

				httpConnection.setRequestMethod("PUT");

				// Construct a wrapper around the actual output stream that does extra processing on close requests.
				// This is the only way to perform processing when the output stream is closed.
				return new FilterOutputStream(connection.getOutputStream()) {

					// Must override 3-arg write for efficiency
					// The default implementation in FilterOutputStream does I/O a byte at a time.
					public void write(byte b[], int off, int len)
							throws IOException {
						out.write(b, off, len);
					}

					// Override close to trigger the transmission and report any errors.
					public void close()
							throws IOException {

						super.close();
						// Note: getResponseCode throws IOException if the response is NotFound
						// and for some others.  It does not throw for missing authentication.
						int retcode = httpConnection.getResponseCode();
						httpConnection.disconnect();
						if (retcode >= 300) {
							// Status code indicates an error.  Throw an IOException.
							String msg = httpConnection.getResponseMessage();
							if (msg == null) {
								switch (retcode) {
									case HttpURLConnection.HTTP_UNAUTHORIZED:
										msg = ResMgr.getMessage(CLASS, 93982, "authentication required");
										break;
									case HttpURLConnection.HTTP_NOT_FOUND:
										msg = ResMgr.getMessage(CLASS, 68612, "not found");
										break;
									default:
										msg = "";
										break;
								}
							}
							throw new IOException(ResMgr.getMessage(CLASS, 10251, "Unable to write to HTTP Server:  Response code {0} {1}",
									new Object[]{ new Integer(retcode),
									msg }));
						}
					}
				};
			}
		}
		catch (IOException e) {
			throw new VariableException(e, new IString(CLASS, 32951, "Error getting URL output stream."));
		}
	}

	/**
	 * Returns an input stream on the configured URL.
	 * This is used when pre-processing input file parameters to copy from the URL to a local file.
	 * It can also be used when viewing the results to see the contents of either an input or output file parameter.
	 * <p><i>Implements interface <b>DataHandler</b></i></p>
	 * @return InputStream
	 * @throws VariableException
	 */
	public InputStream getInputStream()
			throws VariableException {

		try {
			return getConnection().getInputStream();
		}
		catch (UnknownHostException uhe) {
			throw new VariableException(uhe, new IString(CLASS, 63079, "Specified host name not found."));
		}
		catch (MalformedURLException mfe) {
			throw new VariableException(mfe, new IString(CLASS, 890, "Invalid URL format."));
		}
		catch (ConnectException ce) {
			throw new VariableException(ce, new IString(CLASS, 22237, "Connection to remote host failed."));
		}
		catch (IOException e) {
			throw new VariableException(e, new IString(CLASS, 73225, "Error opening URL for reading."));
		}
	}

	/**
	 * Convert the configuration of this handler into a String to be saved into a Model file.
	 * The string uses an XML-like encoding of start and end tags plus text.
	 * This format is parsed using the method Util.getTag(start, end) - see setConfig(String).
	 * <p>
	 * IMPORTANT: the resolved name is NOT stored in the model.  It must be recalculated for each run
	 * of the model.  However it IS saved in the serialized form and the binary form of the handler
	 * so it can be mapped from one component to another.
	 * <p>
	 * NOTE: this method must call super.getConfig(sbuf) to store certain common parts of the configuration.
	 * <p><i>Implements interface <b>DataHandler</b></i></p>
	 * @param sbuf String buffer to append the configuration to.  This buffer may not be empty,
	 * so only append to it.
	 * @throws VariableException
	 */
	public void getConfig(StringBuffer sbuf)
			throws VariableException {

		super.getConfig(sbuf);

		if (rawURLName != null) {
			sbuf.append("<url>");
			sbuf.append(rawURLName);
			sbuf.append("</url>");
		}
		else {
			// Not configured, do nothing and leave sbuf as it is.
		}
	}

	/**
	 * Configure the Handler from a string read from a Model file.  The string will be in the format
	 * produced by getConfig(StringBuffer). An unconfigured handler may get called with an empty configuration,
	 * which should be OK and will leave the handler unconfigured.
	 * <p><i>Implements interface <b>DataHandler</b></i></p>
	 * @param config Configuration read from Model XML.  May be the empty string but will never be null.
	 * @throws VariableException
	 */
	public void setConfig(String config)
			throws VariableException {

		// Let the super-class process any common part of the configuration.
		super.setConfig(config);

		// Make sure the non-preserved part of the configuration is reset.
		resolvedURLName = null;
		url = null;

		// Restore the name.  If the tags are not found, or there is no text between them, null is returned.
		rawURLName = getTag("<url>", "</url>", config);

		// Restore the resolved URL if one is present in the string.
		// It will NOT be present when restoring from a model, but will be present when this method
		// is called by  setConfigBin() to restore the state from the results database.
		resolvedURLName = getTag("<resolved>", "</resolved>", config);

		// Always inform listeners of changes to the configuration
		fireChangeEvent();
	}

	/**
	 * Utility to extract an entry from the pseudo-XML Strings used to store the configuration.
	 * Look for the start and end tags.  If both are found, return the string between them.
	 * Return empty strings as null.
	 * @param startTag Start tag to search for
	 * @param endTag End tag to search for
	 * @param xml Pseudo-xml String to search for tags in
	 * @return String between the tags, or null if the tags are not found, or there is nothing but
	 * white space between them.
	 */
	protected String getTag(String startTag, String endTag, String xml) {

		if ((xml == null) || (xml.length() == 0)) {
			return null;
		}
		int tagLen = startTag.length();

		int start = xml.indexOf(startTag);
		if (start < 0) {
			return null;
		}
		int end = xml.indexOf(endTag, start);
		if ((end < 0) || (end < start + tagLen)) {
			return null;
		}

		String result = xml.substring(start + tagLen, end);
		result = result.trim();

		if (result.length() == 0) {
			return null;
		}

		// Do not return the original substring, as that can result in a reference to a very long
		// string being stored for a long time.  Return a new string instead.
		return new String(result);
	}

	/**
	 * Get the binary form of the handler configuration that is stored in the results database.
	 * The binary form of a handler is restricted by class DataHandler.BinFormat to be one string and an
	 * optional byte array.  There is nothing useful to store in the byte array, so a string format
	 * similar to the results of getConfig(StringBuffer) is used.
	 * <p>
	 * The binary format must include the resolved name, as it is used in the Runtime Gateway to retrieve
	 * exactly the same object as was written to the sever from an output file.
	 * @return DataHandler.BinFormat
	 * @throws VariableException
	 */
	public DataHandler.BinFormat getConfigBin()
			throws VariableException {

		StringBuffer sbuf = new StringBuffer();
		getConfig(sbuf);

		// Add the resolved name to the configuration stored in the database (if there is one)
		if (resolvedURLName != null) {
			sbuf.append("<resolved>").append(resolvedURLName).append("</resolved>");
		}

		// Now return it in the expected format.
		return new DataHandler.BinFormat(sbuf.toString(), null);
	}

	/**
	 * Restore the configuration of the handler from the results database.
	 * Only the String part of the DataHandler.BinFormat is used.
	 * Note that 'setConfig' will restore the resolvedURLName that is added by getConfigBin() if it is there.
	 * @param data Configuration restored from the results database for this handler.
	 * @throws VariableException
	 */
	public void setConfigBin(DataHandler.BinFormat data)
			throws VariableException {
		setConfig(data.getConfigString());
	}

	/**
	 * All handlers must be clonable.  Thismethod just makes sure the URL is not part of the clone.
	 * <p><i>Overrides superclass <b>java.lang.Object</b></i></p>
	 * <p><i>Implements interface <b>DataHandler</b></i></p>
	 * @return Object the clone
	 * @throws CloneNotSupportedException
	 */
	public Object clone()
			throws CloneNotSupportedException {

		DataHandlerURL copy = (DataHandlerURL)super.clone();    // Shallow copy
		copy.url = null;

		return copy;
	}

	/**
	 * Get the resource name for this handler.  This is a convenience method for examining the
	 * configuration of the handler without having to down-cast from DataHandler to a specific handler type.
	 * This is an optional method - a handler that cannot be configured with a single string should return
	 * null here.
	 * <p>
	 * For the URLHandler, the URL itself is the configuration, and it is returned here.
	 * @return String The file-name part of the configuration, if there is one, else null.
	 */
	public String getResourceName() {
		return rawURLName;
	}

	/**
	 * Set the configuration of the handler from a single, simple string, provided the handler can be
	 * configured this way. This is a convenience to avoid having to down-cast a general DataHandler to a
	 * specific handler type in order to configure it.
	 * The File handler accepts a File path (with optional substitutions),
	 * and the URL handler accepts a URL here.  The in-model handler accepts an absolute path and
	 * loads the contents of the file into memory.  Complex handlers like FTP and PDM systems cannot
	 * be configured this way and should throw a VariableException.
	 * @param name String (usually an absolute path) to use to configure
	 * the handler.
	 * @throws VariableException If the handler does not support being
	 * configured via a file name, or if anything goes wrong during configuration.
	 */
	public void setResourceName(String name)
			throws VariableException {

		// This may be called with an empty string or null.  Null is valid but an empty string is not,
		// so convert an empty string into null.
		if ((name != null) && (name.trim().length() == 0)) {
			name = null;
		}

		// Set the raw URL from the string.
		rawURLName = name;

		// Clear the rest of the configuration.
		resolvedURLName = null;
		url = null;

		// Always inform listeners of a change.
		fireChangeEvent(DataHandler.CHANGED_RESOURCE_NAME);
	}

	/**
	 * Tell the handler to remove the resource associated with it.  This is an optional operation - if it is not
	 * supported, just throw VarUnsupportedOperationException.
	 * While some URL's can in theory have their resource removed (such as file: URLs for the local machine),
	 * this method is not implemented.
	 * @throws VariableException
	 */
	//=========================================================================
	public void remove()
			throws VariableException {
		throw new VarUnsupportedOperationException(new IString(CLASS, 82669, "Remove() is not supported for URL data handlers."));
	}

	/**
	 * Is the configuration of this handler safe for parallel execution as an output file handler.
	 * That is, will it write a different file each time getOutputStream() is called?
	 * This is always true for in-model and FIPER File Manager handlers.
	 * It is true for File handlers provided the path involves a {workitem} or {rundir} substitution.
	 * While it isn't strictly true, have this URL handler behave the same as a File handler.
	 * @return boolean
	 */
	public boolean isParallelSafe() {

		return FileUtil.isSafeResourceName(rawURLName);
	}

	/**
	 * Get the size of the file pointed to by this handler.
	 * This is an optional operation - throw VarUnsupportedOperationException if this is not supported.
	 * Since there is no way to find the size of the file a URL will read without reading it,
	 * this method is left unimplemented.
	 * @return long Size of file in bytes
	 * @throws VarUnsupportedOperationException if this operation is not supported.
	 * @throws VariableException
	 */
	public long getFileSize()
			throws VariableException, VarUnsupportedOperationException {

		throw new VarUnsupportedOperationException(new IString(CLASS, 78289, "URL data handler cannot know the size of the file."));
	}
}

