/*******************************************************************************
 * Copyright (c) 2005, 2014 Cognos Incorporated, IBM Corporation and others
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Cognos Incorporated - initial API and implementation
 *     IBM Corporation - bug fixes and enhancements
 *     Raymond Augé <raymond.auge@liferay.com> - Bug 436698
 *     Falko Schumann <falko.schumann@bitctrl.de> - Bug 519955
 *******************************************************************************/
package org.eclipse.equinox.http.servlet.internal.servlet;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.security.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.*;
import org.eclipse.equinox.http.servlet.RangeAwareServletContextHelper;
import org.eclipse.equinox.http.servlet.internal.util.Const;
import org.osgi.service.http.context.ServletContextHelper;

public class ResourceServlet extends HttpServlet {
	private static final long serialVersionUID = 3586876493076122102L;
	private static final String LAST_MODIFIED = "Last-Modified"; //$NON-NLS-1$
	private static final String IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$
	private static final String IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$
	private static final String ETAG = "ETag"; //$NON-NLS-1$
	private static final String RANGE = "Range"; //$NON-NLS-1$
	private static final String ACCEPT_RANGES = "Accept-Ranges"; //$NON-NLS-1$
	private static final String RANGE_UNIT_BYTES = "bytes"; //$NON-NLS-1$
	private static final String CONTENT_RANGE = "Content-Range"; //$NON-NLS-1$

	private final String internalName;
	final ServletContextHelper servletContextHelper;
	private final AccessControlContext acc;

	public ResourceServlet(String internalName, ServletContextHelper servletContextHelper, AccessControlContext acc) {
		if (internalName.equals(Const.SLASH)) {
			internalName = Const.BLANK;
		}
		this.internalName = internalName;
		this.servletContextHelper = servletContextHelper;
		this.acc = acc;
	}

	public void service(HttpServletRequest req, final HttpServletResponse resp) throws IOException {
		String method = req.getMethod();
		if (method.equals("GET") || method.equals("POST") || method.equals("HEAD")) { //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
			String pathInfo = HttpServletRequestWrapperImpl.getDispatchPathInfo(req);
			if (pathInfo == null)
				pathInfo = Const.BLANK;
			String resourcePath = internalName + pathInfo;
			URL resourceURL = servletContextHelper.getResource(resourcePath);
			if (resourceURL != null)
				writeResource(req, resp, resourcePath, resourceURL);
			else
				resp.sendError(HttpServletResponse.SC_NOT_FOUND, "ProxyServlet: " + req.getRequestURI()); //$NON-NLS-1$
		} else {
			resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
		}
	}

	private void writeResource(final HttpServletRequest req, final HttpServletResponse resp, final String resourcePath, final URL resourceURL) throws IOException {
		try {
			AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {

				@Override
				public Boolean run() throws Exception {
					URLConnection connection = resourceURL.openConnection();
					long lastModified = connection.getLastModified();
					int contentLength = connection.getContentLength();

					String etag = null;
					if (lastModified != -1 && contentLength != -1)
						etag = "W/\"" + contentLength + "-" + lastModified + "\""; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$

					// Check for cache revalidation.
					// We should prefer ETag validation as the guarantees are stronger and all HTTP 1.1 clients should be using it
					String ifNoneMatch = req.getHeader(IF_NONE_MATCH);
					if (ifNoneMatch != null && etag != null && ifNoneMatch.indexOf(etag) != -1) {
						resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
						return Boolean.TRUE;
					}

					long ifModifiedSince = req.getDateHeader(IF_MODIFIED_SINCE);
					// for purposes of comparison we add 999 to ifModifiedSince since the fidelity
					// of the IMS header generally doesn't include milli-seconds
					if (ifModifiedSince > -1 && lastModified > 0 && lastModified <= (ifModifiedSince + 999)) {
						resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
						return Boolean.TRUE;
					}

					String rangeHeader = req.getHeader(RANGE);
					Range range = null;
					if (rangeHeader != null) {
						range = Range.createFromRangeHeader(rangeHeader);
						range.completeLength = contentLength;
						range.updateBytePos();

						if (!range.isValid()) {
							resp.setHeader(ACCEPT_RANGES, RANGE_UNIT_BYTES);
							resp.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
							return Boolean.TRUE;
						}
					}

					// return the full contents regularly
					if (contentLength != -1)
						resp.setContentLength(contentLength);

					String filename = new File(resourcePath).getName();
					String contentType = servletContextHelper.getMimeType(filename);
					if (contentType == null)
						contentType = getServletConfig().getServletContext().getMimeType(filename);

					if (contentType != null)
						resp.setContentType(contentType);

					if (lastModified > 0)
						resp.setDateHeader(LAST_MODIFIED, lastModified);

					if (etag != null)
						resp.setHeader(ETAG, etag);

					if (range == null &&
						(servletContextHelper instanceof RangeAwareServletContextHelper) &&
						((RangeAwareServletContextHelper)servletContextHelper).rangeableContentType(contentType, req.getHeader("User-Agent"))) { //$NON-NLS-1$

						range = new Range();
						range.firstBytePos = 0;
						range.completeLength = contentLength;
						range.updateBytePos();
					}

					if (range != null) {
						resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
						resp.setHeader(ACCEPT_RANGES, RANGE_UNIT_BYTES);
						resp.setContentLength(range.contentLength());
						resp.setHeader(CONTENT_RANGE, RANGE_UNIT_BYTES + " " + range.firstBytePos + "-" + range.lastBytePos + "/" + range.completeLength); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					}

					if (contentLength != 0) {
						// open the input stream
						try (InputStream is = connection.getInputStream()) {
							// write the resource
							try {
								OutputStream os = resp.getOutputStream();
								int writtenContentLength = writeResourceToOutputStream(is, os, range);
								if (contentLength == -1 || contentLength != writtenContentLength)
									resp.setContentLength(writtenContentLength);
							} catch (IllegalStateException e) { // can occur if the response output is already open as a Writer
								Writer writer = resp.getWriter();
								writeResourceToWriter(is, writer, range);
								// Since ContentLength is a measure of the number of bytes contained in the body
								// of a message when we use a Writer we lose control of the exact byte count and
								// defer the problem to the Servlet Engine's Writer implementation.
							}
						} catch (FileNotFoundException e) {
							// FileNotFoundException may indicate the following scenarios
							// - url is a directory
							// - url is not accessible
							sendError(resp, HttpServletResponse.SC_FORBIDDEN);
						} catch (SecurityException e) {
							// SecurityException may indicate the following scenarios
							// - url is not accessible
							sendError(resp, HttpServletResponse.SC_FORBIDDEN);
						}
					}
					return Boolean.TRUE;
				}
			}, acc);
		} catch (PrivilegedActionException e) {
			throw (IOException) e.getException();
		}
	}

	void sendError(final HttpServletResponse resp, int sc) throws IOException {

		try {
			// we need to reset headers for 302 and 403
			resp.reset();
			resp.sendError(sc);
		} catch (IllegalStateException e) {
			// this could happen if the response has already been committed
		}
	}

	int writeResourceToOutputStream(InputStream is, OutputStream os, Range range) throws IOException {
		if (range != null) {
			if (range.firstBytePos != Range.NOT_SET) {
				is.skip(range.firstBytePos);
			} else {
				is.skip(range.completeLength - range.lastBytePos);
			}
		}

		byte[] buffer = new byte[8192];
		int bytesRead = is.read(buffer);
		int writtenContentLength = 0;
		while (bytesRead != -1 && (range == null || range.lastBytePos == Range.NOT_SET || writtenContentLength < range.lastBytePos)) {
			if (range != null && range.lastBytePos != Range.NOT_SET && (bytesRead + writtenContentLength) > range.lastBytePos) {
				bytesRead = range.contentLength() - writtenContentLength;
			}
			os.write(buffer, 0, bytesRead);
			writtenContentLength += bytesRead;
			bytesRead = is.read(buffer);
		}
		return writtenContentLength;
	}

	void writeResourceToWriter(InputStream is, Writer writer, Range range) throws IOException {
		if (range != null) {
			if (range.firstBytePos != Range.NOT_SET) {
				is.skip(range.firstBytePos);
			} else {
				is.skip(range.completeLength - range.lastBytePos);
			}
		}

		try (Reader reader = new InputStreamReader(is)) {
			char[] buffer = new char[8192];
			int charsRead = reader.read(buffer);
			int writtenContentLength = 0;
			while (charsRead != -1 && (range == null || range.lastBytePos == Range.NOT_SET || writtenContentLength < range.lastBytePos)) {
				if (range != null && range.lastBytePos != Range.NOT_SET && (charsRead + writtenContentLength) > range.lastBytePos) {
					charsRead = range.contentLength() - writtenContentLength;
				}
				writer.write(buffer, 0, charsRead);
				writtenContentLength += charsRead;
				charsRead = reader.read(buffer);
			}
		}
	}

	static class Range {

		private static final Pattern RANGE_PATTERN = Pattern.compile("^(.+)=(\\d+)?-(\\d+)?$"); //$NON-NLS-1$

		static final int NOT_SET = -1;

		String rangeUnit = RANGE_UNIT_BYTES;
		int firstBytePos = NOT_SET;
		int lastBytePos = NOT_SET;
		int completeLength = NOT_SET;

		static Range createFromRangeHeader(String header) {
			Range range = new Range();

			Matcher matcher = RANGE_PATTERN.matcher(header);
			if (!matcher.matches()) {
				// use default values
				return range;
			}

			range.rangeUnit = matcher.group(1);
			if (matcher.group(2) != null) {
				try {
					range.firstBytePos = Integer.parseInt(matcher.group(2));
				} catch (NumberFormatException ignored) {
					// use default value
				}
			}
			if (matcher.group(3) != null) {
				try {
					range.lastBytePos = Integer.parseInt(matcher.group(3));
				} catch (NumberFormatException ignored) {
					// use default value
				}
			}
			return range;
		}

		void updateBytePos() {
			if (lastBytePos == -1 || lastBytePos >= completeLength) {
				lastBytePos = completeLength - 1;
			}
			if (firstBytePos == -1) {
				firstBytePos = completeLength - lastBytePos - 1;
				lastBytePos = completeLength - 1;
			}
		}

		boolean isValid() {
			// we accept bytes unit only
			if (!RANGE_UNIT_BYTES.equals(rangeUnit)) {
				return false;
			}

			if (firstBytePos == NOT_SET && lastBytePos == NOT_SET) {
				return false;
			}

			if (firstBytePos >= completeLength) {
				return false;
			}

			if (lastBytePos >= completeLength) {
				return false;
			}

			return true;
		}

		int contentLength() {
			return lastBytePos - firstBytePos + 1;
		}

	}

}
