/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.adapter.jms.service;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.uima.UIMAFramework;
import org.apache.uima.aae.UIMAEE_Constants;
import org.apache.uima.adapter.jms.JmsConstants;
import org.apache.uima.internal.util.XMLUtils;
import org.apache.uima.util.Level;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

public class Dd2spring {

	private static final Class<Dd2spring> THIS_CLASS = Dd2spring.class;

	private ClassLoader saxonClassLoader;

	/**
	 * Test driver arg = path_to_source, path_to_xslt, path_to_saxon_jar,
	 * uima-as-debug flag
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			new Dd2spring()
					.convertDd2Spring(args[0], args[1], args[2], args[3]);

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public File convertDd2Spring(String ddFilePath,
			String dd2SpringXsltFilePath, String saxonClasspath,
			String uimaAsDebug) throws Exception {

		URL urlForSaxonClassPath;
		try {
			urlForSaxonClassPath = new URL(saxonClasspath);
		} catch (MalformedURLException e) {
			e.printStackTrace();
			UIMAFramework
					.getLogger(THIS_CLASS)
					.logrb(Level.CONFIG,
							THIS_CLASS.getName(),
							"convertDD2Spring",
							JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
							"UIMA_dd2spring_Cannot_convert_saxon_classpath_to_a_URL_SEVERE",
							new Object[] { saxonClasspath });
			return null;
		}

		File tempFile;
		try {
			tempFile = File.createTempFile("UIMAdd2springOutput", ".xml");
		} catch (IOException e) {
			e.printStackTrace();
			UIMAFramework.getLogger(THIS_CLASS).logrb(Level.CONFIG,
					THIS_CLASS.getName(), "convertDD2Spring",
					JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
					"UIMA_dd2spring_cant_create_temp_output_file_SEVERE");
			return null;
		}

		// UIMA-5022 No longer capture output and scan for "ERROR: " as that hid
		// errors that Saxon followed by calling exit!
		// Processing now terminates on the first error.
		convertDd2Spring(tempFile, ddFilePath, dd2SpringXsltFilePath,
				urlForSaxonClassPath);

		// delete the file when terminating if
		// a) uimaAsDebug is not specified (is null) or
		// b) uimaAsDebug is specified, but is ""
		if (null == uimaAsDebug || uimaAsDebug.equals("")) {
			tempFile.deleteOnExit();
		}

		return tempFile;
	}

	private void testForSaxon9InClasspath() throws Exception {
		// UIMA-5117 Check for saxon9. If it is in the users's classpath an NPE
		// is thrown in
		// net.sf.saxon.event.ReceivingContentHandler.getNodeName while handling
		// a getMeta request.
		try {
			Class<?> saxonVersionClass = Class.forName("net.sf.saxon.Version");
			Method versionMethod = saxonVersionClass
					.getMethod("getProductVersion");
			String version = (String) versionMethod.invoke(null);
			if (version.startsWith("9")) {
				UIMAFramework.getLogger(THIS_CLASS).logrb(Level.SEVERE,
						THIS_CLASS.getName(), "convertDD2Spring",
						UIMAEE_Constants.JMS_LOG_RESOURCE_BUNDLE,
						"UIMAEE_exception__SEVERE",
						new Object[] { "saxon9 must not be in classpath" });
				throw new Dd2springException(
						"saxon9 found in classpath - dd2spring transformation and UIMA-AS do not support saxon9");
			}
		} catch (ClassNotFoundException e) {
			// OK - saxon not in classpath
		}
	}

	/**
	 * 
	 * @param tempFile
	 *            file to hold generated Spring from dd2spring transform
	 * @param ddFilePath
	 *            file path to UIMA Deployment Descriptor - passed to saxon
	 * @param dd2SpringXsltFilePath
	 *            file path to dd2spring.xslt transformation file - passed to
	 *            saxon
	 * @param saxonClasspathURL
	 *            classpath for saxon8.jar
	 */
	public void convertDd2Spring(File tempFile, String ddFilePath,
			String dd2SpringXsltFilePath, URL saxonClasspathURL)
			throws Exception {

		testForSaxon9InClasspath();

		// UIMA-5117 - Add shutdown hook so can log when saxon gives up and
		// calls exit :(
		ShutdownHook shutdownHook = new ShutdownHook();
		Runtime.getRuntime().addShutdownHook(shutdownHook);

		// Create a classloader with saxon8 that delegates to the user's
		// classloader
		ClassLoader currentClassloader = Thread.currentThread()
				.getContextClassLoader();
		if (null == saxonClassLoader) {
			URL[] classLoaderUrls = new URL[] { saxonClasspathURL };
			saxonClassLoader = new URLClassLoader(classLoaderUrls,
					currentClassloader);
		}
		// configure Saxon with these settings
		SaxonInputs saxonConfig = new SaxonInputs(ddFilePath, tempFile,
				dd2SpringXsltFilePath, saxonClassLoader, currentClassloader);
		
		// creates either command line or java based interface to Saxon
		SaxonInterface saxon = SaxonInterfaceFactory.newSaxonInterface(
				saxonConfig);

		try {

			saxon.convertDD2Spring();

		} catch (ClassNotFoundException e) {
			System.err.println("Error - can't load Saxon jar from "
					+ saxonClasspathURL + " for dd2spring transformation.");
			e.printStackTrace();
			UIMAFramework.getLogger(THIS_CLASS).logrb(Level.SEVERE,
					THIS_CLASS.getName(), "convertDD2Spring",
					JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
					"UIMA_dd2spring_saxon_missing_SEVERE");
			throw e;
		} catch (Exception e) {
			System.err.println("Error - dd2spring transformation failed:");
			e.printStackTrace();
			UIMAFramework.getLogger(THIS_CLASS).logrb(Level.SEVERE,
					THIS_CLASS.getName(), "convertDD2Spring",
					JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
					"UIMA_dd2spring_internal_error_calling_saxon");
			throw e;
		} finally {
			Runtime.getRuntime().removeShutdownHook(shutdownHook);
		}
		return;
	}

	// Shutdown hook that reports when Saxon calls exit!
	private class ShutdownHook extends Thread {
		public void run() {
			System.err
					.println("ERROR in dd2spring Saxon transformation ... System.exit called");
			System.err.flush();
		}
	}

	/**
	 * The SaxonInputs class is used to configure Saxon
	 *
	 */
	private class SaxonInputs {
		String ddFilePath;
		File springFilePath;
		String xsltPath;
		ClassLoader currentClassLoader;
		ClassLoader saxonClassLoader;
		public SaxonInputs(String ddFilePath, File springFilePath,
				String xsltPath, ClassLoader saxonClassloader,
				ClassLoader currentClassloader) {
			super();
			this.ddFilePath = ddFilePath;
			this.springFilePath = springFilePath;
			this.xsltPath = xsltPath;
			this.currentClassLoader = currentClassloader;
			this.saxonClassLoader = saxonClassloader;
		}

		public ClassLoader getCurrentClassLoader() {
			return currentClassLoader;
		}
		public ClassLoader getSaxonClassLoader() {
			return saxonClassLoader;
		}

		public String getDDFilePath() {
			return ddFilePath;
		}

		public File getSpringFilePath() {
			return springFilePath;
		}

		public String getXSLTPath() {
			return xsltPath;
		}
	}
	// Factory to produce desired Saxon interface. Currently two
	// are supported: command line based and java API based. The
	// default is java API interface. The command line is used
	// to provide Saxon a custom parser.
	private static class SaxonInterfaceFactory {
		public static SaxonInterface newSaxonInterface(	SaxonInputs saxonConfig) {
			SaxonInterface saxon;
			if (System.getProperty("uima.framework_impl") != null) {
				UIMAFramework.getLogger(THIS_CLASS).log(Level.INFO,
					     "Using Saxon command line interface - Java Vendor:"+System.getProperty("java.vendor"));
				// The command line based Saxon interface is used to plug-in
				// custom parser into Saxon
				saxon = new SaxonCommandLineInterface(saxonConfig);
			} else {
				UIMAFramework.getLogger(THIS_CLASS).log(Level.INFO,
				     "Using Saxon Java API - Java Vendor:"+System.getProperty("java.vendor"));
				// Default, use java API based Saxon interface
				saxon = new SaxonJavaInterface(saxonConfig);
			}
			return saxon;
		}
	}

	private interface SaxonInterface {
		public void convertDD2Spring() throws Exception;
	}

	private static class SaxonCommandLineInterface implements SaxonInterface {
		private List<String> argsForSaxon = new ArrayList<String>();
		private SaxonInputs saxonConfig;
		
		SaxonCommandLineInterface(SaxonInputs saxonConfig) {
			// args for saxon
			// -l -s deployment_descriptor} -o output_file_path
			// dd2spring.xsl_file_path <-x sax_parser_class>
			// If a custom framework includes a custom XML parser we may also
			// need a custom parser for Saxon,
			// so check for the existence of a class with "_SAXParser" appended
			// to the framework name.
			this.saxonConfig = saxonConfig;
			argsForSaxon.add("-l"); // turn on line numbers
			argsForSaxon.add("-s"); // source file
			argsForSaxon.add(saxonConfig.getDDFilePath()); // source file
			argsForSaxon.add("-o"); // output file
			argsForSaxon.add(saxonConfig.getSpringFilePath().getAbsolutePath()); // output
																					// file
			argsForSaxon.add(saxonConfig.getXSLTPath()); // xslt transform to
															// apply
			// test if there is a custom parser defined
			String uimaFrameworkClass = System
					.getProperty("uima.framework_impl");

			if (uimaFrameworkClass != null) {
				String saxonParserClass = uimaFrameworkClass + "_SAXParser";
				try {
					saxonConfig.getSaxonClassLoader().loadClass(saxonParserClass);
					argsForSaxon.add("-x");
					argsForSaxon.add(saxonParserClass);
				} catch (ClassNotFoundException e) {
					// No parser class defined
				}
			}
			UIMAFramework.getLogger(THIS_CLASS).log(Level.INFO,
					"Saxon args: " + argsForSaxon);

		}

		public void convertDD2Spring() throws Exception {
			try {
				// Set the thread classloader so that all classes are loaded from this
				Thread.currentThread().setContextClassLoader(saxonConfig.getSaxonClassLoader());
				Class<?> mainStartClass = null;
				mainStartClass = Class.forName("net.sf.saxon.Transform", true,
						saxonConfig.getSaxonClassLoader());
				Method mainMethod = mainStartClass
						.getMethod("main", String[].class);
				mainMethod.invoke(null, new Object[] { argsForSaxon
						.toArray(new String[argsForSaxon.size()]) });

			} finally {
				// Restore original classloader and remove used shutdown hook
				Thread.currentThread().setContextClassLoader(saxonConfig.getCurrentClassLoader());

			}
		}

	}

	private static class SaxonJavaInterface implements SaxonInterface {
		private SaxonInputs saxonConfig;

		SaxonJavaInterface(SaxonInputs saxonConfig) {
			this.saxonConfig = saxonConfig;
		}
		private void setFeature(Method setFeatureMethod, Object xmlReaderObject, String feature, boolean flag) {
			try{
				setFeatureMethod.invoke(xmlReaderObject, feature, flag );
			} catch( Exception e) {
				UIMAFramework.getLogger(THIS_CLASS).log(Level.WARNING,"XMLReader didn't recognize feature "+feature);
			}
		}
		private void configure(Object xmlReaderObject) throws Exception {
			Class<?> xmlReaderClass = Class.forName("org.xml.sax.XMLReader",
					true, saxonConfig.getSaxonClassLoader());
			Method setFeatureMethod = xmlReaderClass.getMethod("setFeature",
					new Class[] { String.class, boolean.class });
			setFeature(setFeatureMethod,xmlReaderObject,"http://xml.org/sax/features/external-general-entities", false);
			setFeature(setFeatureMethod,xmlReaderObject,"http://xml.org/sax/features/external-parameter-entities", false);
			setFeature(setFeatureMethod,xmlReaderObject,"http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
			setFeature(setFeatureMethod,xmlReaderObject,"http://apache.org/xml/features/disallow-doctype-decl", true);
		}

		private void beforeProcess() {
			/* *********************************************************************************** */
			/*
			 * The following properties are required by Saxon. Dont remove these
			 * settings
			 */
			/* *********************************************************************************** */

			System.setProperty("javax.xml.transform.TransformerFactory",
					"net.sf.saxon.TransformerFactoryImpl");

		}

		private void afterProcess() {
			/* *********************************************************************************** */
			/*
			 * Don't remove both clearProperty() calls below. Downstream parsers
			 * may get confused.
			 */
			/* *********************************************************************************** */
			System.clearProperty("javax.xml.parsers.SAXParserFactory");
			System.clearProperty("javax.xml.transform.TransformerFactory");
			/* *********************************************************************************** */

		}

		public void convertDD2Spring() throws Exception {
			beforeProcess();
			BufferedWriter writer = null;
			try {
				// Set the thread classloader so that all classes are loaded from this
				Thread.currentThread().setContextClassLoader(saxonConfig.getSaxonClassLoader());

				Class<?> configClass = Class.forName(
						"net.sf.saxon.Configuration", true, saxonConfig.getSaxonClassLoader());
				Object oc = configClass.newInstance();
				Method getXMLReaderMethod = configClass
						.getMethod("getStyleParser");

				// get Saxon xml parser
				Object xmlReaderObject = getXMLReaderMethod.invoke(oc);
				configure(xmlReaderObject);
				StringWriter out = new StringWriter();

				//TransformerFactory tFactory = TransformerFactory.newInstance();
				TransformerFactory tFactory = 
					XMLUtils.createSaxTransformerFactory();
				StreamSource xslSource = new StreamSource(new File(
						saxonConfig.getXSLTPath()));
				StreamResult xmlResult = new StreamResult(out);
				Transformer transformer = tFactory.newTransformer(xslSource);
				// Need an absolute file path for the dd. This is fed to
				// InputSource below
				File ddAbsFilePath = new File(saxonConfig.getDDFilePath());
				// System.out.println("DD:"+ddAbsFilePath.getAbsolutePath());
				// Must be URI otherwise the transform fails on windows with
				// DynamicError. 
				SAXSource source = new SAXSource(new InputSource(
						ddAbsFilePath.toURI().toString())); 

				source.setXMLReader((XMLReader) xmlReaderObject);

				transformer.transform(source, xmlResult);
				// Save transformed DD for loading into Spring Framework
				writer = new BufferedWriter(new FileWriter(
						saxonConfig.getSpringFilePath()));
				writer.write(out.toString());
				
				if ( UIMAFramework
					.getLogger(THIS_CLASS)
					.isLoggable(Level.FINEST) ) {
					System.out.println("Finished Writing to temp file:"
							+ saxonConfig.getSpringFilePath().getAbsolutePath());
					String result = out.toString();
					System.out.println(result);

				}

			} finally {
				if ( writer != null ) {
					try {
						writer.close();
					} catch(Exception e){}
				}

				afterProcess();
				// Restore original classloader and remove used shutdown hook
				Thread.currentThread().setContextClassLoader(saxonConfig.getCurrentClassLoader());

			}

		}

	}
}
