/*
 * 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.onehippo.cocoon.components.reading;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Map;

import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.caching.CacheableProcessingComponent;
import org.apache.cocoon.components.source.SourceUtil;
import org.apache.cocoon.environment.Context;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Response;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceException;
import org.apache.excalibur.source.SourceValidity;
import org.inconspicuous.jsmin.JSMin;
import org.xml.sax.SAXException;

/**
 * 
 * The <code>JavaScriptMinifyReader</code> component is used to serve Javascript
 * data in a sitemap pipeline. It makes use of HTTP Headers to determine if the
 * requested resource should be written to the <code>OutputStream</code> or if
 * it can signal that it hasn't changed. This reader uses the JSMin class
 * provided by John Reilly See the following url: <a
 * ref="http://www.inconspicuous.org/projects/jsmin/jsmin.java"
 * >http://www.inconspicuous.org/projects/jsmin/jsmin.java</a>.
 * 
 * <p>
 * Configuration:
 * <dl>
 * <dt>&lt;expires&gt;</dt>
 * <dd>This parameter is optional. When specified it determines how long in
 * miliseconds the resources can be cached by any proxy or browser between
 * Cocoon and the requesting visitor. Defaults to -1.</dd>
 * </dl>
 * </p>
 * 
 * <p>
 * Default configuration:
 * 
 * <pre>
 *   &lt;expires&gt;-1&lt;/expires&gt;
 * </pre>
 * 
 * </p>
 * 
 * <p>
 * In addition to reader configuration, above parameters can be passed to the
 * reader at the time when it is used.
 * </p>
 * 
 * @author Jeroen Reijn
 * 
 */

public class JavaScriptMinifyReader extends AbstractReader implements CacheableProcessingComponent, Configurable {

	protected long configuredExpires;
	protected long expires;

	protected int bufferSize = 8192;

	protected Response response;
	protected Request request;
	protected Source inputSource;

	/**
	 * Read reader configuration
	 */
	public void configure(Configuration configuration) throws ConfigurationException {
		final Parameters parameters = Parameters.fromConfiguration(configuration);
		this.configuredExpires = parameters.getParameterAsLong("expires", -1);

		// Configuration has precedence over parameters.
		this.configuredExpires = configuration.getChild("expires").getValueAsLong(configuredExpires);
	}

	/**
	 * Setup the reader. The resource is opened to get an
	 * <code>InputStream</code>, the length and the last modification date
	 */
	public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) 
		throws ProcessingException, SAXException, IOException {
		
		super.setup(resolver, objectModel, src, par);

		this.request = ObjectModelHelper.getRequest(objectModel);
		this.response = ObjectModelHelper.getResponse(objectModel);

		this.expires = par.getParameterAsLong("expires", this.configuredExpires);

		try {
			this.inputSource = resolver.resolveURI(src);
		} catch (SourceException e) {
			throw SourceUtil.handle("Error during resolving of '" + src + "'.",e);
		}
		setupHeaders();
	}

	/**
	 * Generates the requested resource.
	 */
	public void generate() throws IOException, ProcessingException {
		try {
			InputStream inputStream;
			try {
				inputStream = inputSource.getInputStream();
			} catch (SourceException e) {
				throw SourceUtil.handle("Error during resolving of the input stream", e);
			}

			// Bugzilla Bug #25069: Close inputStream in finally block.
			try {
				processStream(inputStream);
			} finally {
				if (inputStream != null) {
					inputStream.close();
				}
			}

		} catch (IOException e) {
			getLogger().debug("Received an IOException, assuming client severed connection on purpose");
		}
		out.flush();
	}

	/**
	 * Process the InputStream.
	 * 
	 * @param inputStream the input stream needed to minify the javascript resource
	 * @throws IOException
	 */
	private void processStream(final InputStream inputStream) throws IOException {

		final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		final JSMin jsMin = new JSMin(inputStream, outputStream);

		try {
			jsMin.jsmin();
		} catch (IOException e) {
			getLogger().debug("Received an IOException, assuming client severed connection on purpose");
		} catch (JSMin.UnterminatedRegExpLiteralException e) {
			getLogger().debug("Received an UnterminatedRegExpLiteralException");
		} catch (JSMin.UnterminatedCommentException e) {
			getLogger().debug("Received an UnterminatedCommentException");
		} catch (JSMin.UnterminatedStringLiteralException e) {
			getLogger().debug("Received an UnterminatedStringLiteralException");
		}

		final int contentLength = outputStream.size();
		if (contentLength != -1) {
			response.setHeader("Content-Length", Integer.toString(contentLength));
		}

		try {
			outputStream.writeTo(out);
		} catch (IOException e) {
			getLogger().debug("Received an IOException, assuming client severed connection on purpose");
		} finally {
			if (outputStream != null) {
				outputStream.close();
			}
		}

	}

	/**
	 * Returns the mime-type of the resource in process.
	 */
	public String getMimeType() {
		final Context ctx = ObjectModelHelper.getContext(objectModel);
		if (ctx != null) {
			final String mimeType = ctx.getMimeType(source);
			if (mimeType != null) {
				return mimeType;
			}
		}
		return inputSource.getMimeType();
	}

	/**
	 * Setup the response headers: Expires
	 */
	protected void setupHeaders() {
		if (expires > 0) {
			response.setDateHeader("Expires", System.currentTimeMillis() + expires);
		} else if (expires == 0) {
			response.setDateHeader("Expires", 0);
		}
	}

	public Serializable getKey() {
		return inputSource.getURI();
	}

	/**
	 * Generate the validity for this source
	 */
	public SourceValidity getValidity() {
		return inputSource.getValidity();
	}

	/**
	 * Recyclable
	 */
	public void recycle() {
		this.request = null;
		this.response = null;
		if (this.inputSource != null) {
			super.resolver.release(this.inputSource);
			this.inputSource = null;
		}
		super.recycle();
	}
}


