/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 *
 *
 * This file incorporates work covered by the following copyright and
 * permission notice:
 *
 * Copyright 2004 The Apache Software Foundation
 *
 * Licensed 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.jasper.compiler;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Stack;
import java.util.jar.JarFile;

import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;
import org.apache.jasper.xmlparser.XMLEncodingDetector;
import org.xml.sax.Attributes;

/**
 * Controller for the parsing of a JSP page.
 * <p>
 * The same ParserController instance is used for a JSP page and any JSP
 * segments included by it (via an include directive), where each segment may
 * be provided in standard or XML syntax. This class selects and invokes the
 * appropriate parser for the JSP page and its included segments.
 *
 * @author Pierre Delisle
 * @author Jan Luehe
 */
class ParserController implements TagConstants {

    private static final String CHARSET = "charset=";

    private JspCompilationContext ctxt;
    private Compiler compiler;
    private ErrorDispatcher err;

    /*
     * Indicates the syntax (XML or standard) of the file being processed
     */
    private boolean isXml;

    /*
     * A stack to keep track of the 'current base directory'
     * for include directives that refer to relative paths.
     */
    private Stack<String> baseDirStack = new Stack<String>();

    private boolean isEncodingSpecifiedInProlog;

    private boolean hasBom;

    private String sourceEnc;

    private boolean isDefaultPageEncoding;
    private boolean isTagFile;
    private boolean directiveOnly;

    /*
     * Constructor
     */
    public ParserController(JspCompilationContext ctxt, Compiler compiler) {
        this.ctxt = ctxt;
	this.compiler = compiler;
	this.err = compiler.getErrorDispatcher();
    }

    public JspCompilationContext getJspCompilationContext () {
	return ctxt;
    }

    public Compiler getCompiler () {
	return compiler;
    }

    /**
     * Parses a JSP page or tag file. This is invoked by the compiler.
     *
     * @param inFileName The path to the JSP page or tag file to be parsed.
     */
    public Node.Nodes parse(String inFileName)
	        throws FileNotFoundException, JasperException, IOException {
	// If we're parsing a packaged tag file or a resource included by it
	// (using an include directive), ctxt.getTagFileJar() returns the
	// JAR file from which to read the tag file or included resource,
	// respectively.
        isTagFile = ctxt.isTagFile();
        directiveOnly = false;
        return doParse(inFileName, null, ctxt.getTagFileJarUrl());
    }

    /**
     * Processes an include directive with the given path.
     *
     * @param inFileName The path to the resource to be included.
     * @param parent The parent node of the include directive.
     * @param jarFile The JAR file from which to read the included resource,
     * or null of the included resource is to be read from the filesystem
     */
    public Node.Nodes parse(String inFileName, Node parent,
			    URL jarFileUrl)
	        throws FileNotFoundException, JasperException, IOException {
        // For files that are statically included, isTagfile and directiveOnly
        // remain unchanged.
        return doParse(inFileName, parent, jarFileUrl);
    }

    /**
     * Extracts tag file directive information from the tag file with the
     * given name.
     *
     * This is invoked by the compiler
     *
     * @param inFileName The name of the tag file to be parsed.
     */
    public Node.Nodes parseTagFileDirectives(String inFileName)
	        throws FileNotFoundException, JasperException, IOException {
        boolean isTagFileSave = isTagFile;
        boolean directiveOnlySave = directiveOnly;
        isTagFile = true;
        directiveOnly = true;
        Node.Nodes page = doParse(inFileName, null,
                             (URL) ctxt.getTagFileJarUrls().get(inFileName));
        directiveOnly = directiveOnlySave;
        isTagFile = isTagFileSave;
        return page;
    }

    /**
     * Parses the JSP page or tag file with the given path name.
     *
     * @param inFileName The name of the JSP page or tag file to be parsed.
     * @param parent The parent node (non-null when processing an include
     * directive)
     * @param isTagFile true if file to be parsed is tag file, and false if it
     * is a regular JSP page
     * @param directivesOnly true if the file to be parsed is a tag file and
     * we are only interested in the directives needed for constructing a
     * TagFileInfo.
     * @param jarFile The JAR file from which to read the JSP page or tag file,
     * or null if the JSP page or tag file is to be read from the filesystem
     */
    private Node.Nodes doParse(String inFileName,
                               Node parent,
                               URL jarFileUrl)
	        throws FileNotFoundException, JasperException, IOException {

	Node.Nodes parsedPage = null;
	isEncodingSpecifiedInProlog = false;
	isDefaultPageEncoding = false;
        hasBom = false;

	JarFile jarFile = getJarFile(jarFileUrl);
	String absFileName = resolveFileName(inFileName);
	String jspConfigPageEnc = getJspConfigPageEncoding(absFileName);

	// Figure out what type of JSP document and encoding type we are
	// dealing with
	determineSyntaxAndEncoding(absFileName, jarFile, jspConfigPageEnc);

	if (parent != null) {
	    // Included resource, add to dependent list
	    compiler.getPageInfo().addDependant(absFileName);
	}

        comparePageEncodings(jspConfigPageEnc);

	// Dispatch to the appropriate parser
	if (isXml) {
	    // JSP document (XML syntax)
            // InputStream for jspx page is created and properly closed in
            // JspDocumentParser.
            parsedPage = JspDocumentParser.parse(this, absFileName,
                                                 jarFile, parent,
                                                 isTagFile, directiveOnly,
                                                 sourceEnc,
                                                 jspConfigPageEnc,
                                                 isEncodingSpecifiedInProlog);
	} else {
	    // Standard syntax
	    InputStreamReader inStreamReader = null;
	    try {
		inStreamReader = JspUtil.getReader(absFileName, sourceEnc,
						   jarFile, ctxt, err);
		JspReader jspReader = new JspReader(ctxt, absFileName,
						    sourceEnc, inStreamReader,
						    err);
                parsedPage = Parser.parse(this, absFileName, jspReader, parent,
                                          isTagFile, directiveOnly, jarFileUrl,
					  sourceEnc, jspConfigPageEnc,
					  isDefaultPageEncoding, hasBom);
            } finally {
		if (inStreamReader != null) {
		    try {
			inStreamReader.close();
		    } catch (Exception any) {
		    }
		}
	    }
	}

	if (jarFile != null) {
	    try {
		jarFile.close();
	    } catch (Throwable t) {}
	}

	baseDirStack.pop();

	return parsedPage;
    }

    /*
     * Ensures that the page encoding specified in the JSP config element
     * (with matching URL pattern), if present, matches the page encoding
     * specified in the XML prolog of a JSP document (XML syntax) and the
     * page encoding derived from the BOM.
     *
     * @param jspConfigPageEnc Page encoding specified in JSP config element
     *
     * @throws JasperException if page encoding mismatch
     */
    private void comparePageEncodings(String jspConfigPageEnc)
            throws JasperException {

        if (jspConfigPageEnc == null) {
            return;
        }

        if (isXml && isEncodingSpecifiedInProlog) {
            /*
             * Make sure the encoding specified in the XML prolog matches
             * that in the JSP config element, treating "UTF-16", "UTF-16BE",
             * and "UTF-16LE" as identical.
             */
            if (!jspConfigPageEnc.equalsIgnoreCase(sourceEnc)
                    && (!jspConfigPageEnc.toLowerCase().startsWith("utf-16")
                        || !sourceEnc.toLowerCase().startsWith("utf-16"))) {
                err.jspError("jsp.error.prolog_config_encoding_mismatch",
                             sourceEnc, jspConfigPageEnc);
            }
        }

        if (hasBom) {
            /*
             * Make sure the encoding specified in the BOM matches
             * that in the JSP config element, treating "UTF-16", "UTF-16BE",
             * and "UTF-16LE" as identical.
             */
            if (!jspConfigPageEnc.equalsIgnoreCase(sourceEnc)
                    && (!jspConfigPageEnc.toLowerCase().startsWith("utf-16")
                        || !sourceEnc.toLowerCase().startsWith("utf-16"))) {
                err.jspError("jsp.error.bom_config_encoding_mismatch",
                             sourceEnc, jspConfigPageEnc);
            }
        }
    }

    /*
     * Checks to see if the given URI is matched by a URL pattern specified in
     * a jsp-property-group in web.xml, and if so, returns the value of the
     * <page-encoding> element.
     *
     * @param absFileName The URI to match
     *
     * @return The value of the <page-encoding> attribute of the
     * jsp-property-group with matching URL pattern
     */
    private String getJspConfigPageEncoding(String absFileName)
            throws JasperException {

	JspConfig jspConfig = ctxt.getOptions().getJspConfig();
	JspProperty jspProperty = jspConfig.findJspProperty(absFileName);
	return jspProperty.getPageEncoding();
    }

    /**
     * Determines the syntax (standard or XML) and page encoding properties
     * for the given file, and stores them in the 'isXml' and 'sourceEnc'
     * instance variables, respectively.
     */
    private void determineSyntaxAndEncoding(String absFileName,
					    JarFile jarFile,
					    String jspConfigPageEnc)
	        throws JasperException, IOException {

	isXml = false;

	/*
	 * 'true' if the syntax (XML or standard) of the file is given
	 * from external information: either via a JSP configuration element,
	 * the ".jspx" suffix, or the enclosing file (for included resources)
	 */
	boolean isExternal = false;

	/*
	 * Indicates whether we need to revert from temporary usage of
	 * "ISO-8859-1" back to "UTF-8"
	 */
	boolean revert = false;

        JspConfig jspConfig = ctxt.getOptions().getJspConfig();
        JspProperty jspProperty = jspConfig.findJspProperty(absFileName);
        if (jspProperty.isXml() != null) {
            // If <is-xml> is specified in a <jsp-property-group>, it is used.
            isXml = JspUtil.booleanValue(jspProperty.isXml());
	    isExternal = true;
	} else if (absFileName.endsWith(".jspx")
		   || absFileName.endsWith(".tagx")) {
	    isXml = true;
	    isExternal = true;
	}

	if (isExternal && !isXml) {
	    // JSP (standard) syntax. Use encoding specified in jsp-config
	    // if provided.
	    sourceEnc = jspConfigPageEnc;
	    if (sourceEnc != null) {
		return;
	    }
	    // We don't know the encoding
	    sourceEnc = "ISO-8859-1";
	} else {
	    // XML syntax or unknown, (auto)detect encoding ...
	    Object[] ret = XMLEncodingDetector.getEncoding(absFileName,
							   jarFile, ctxt, err);
	    sourceEnc = (String) ret[0];
	    if (((Boolean) ret[1]).booleanValue()) {
		isEncodingSpecifiedInProlog = true;
	    }
	    if (ret[2] != null && ((Boolean) ret[2]).booleanValue()) {
                hasBom = true;
	    }

	    if (!isXml && sourceEnc.equalsIgnoreCase("utf-8") && !hasBom) {
		/*
		 * We don't know if we're dealing with XML or standard syntax.
		 * Therefore, we need to check to see if the page contains
		 * a <jsp:root> element.
		 *
                 * We need to be careful, because the page may be encoded in
                 * ISO-8859-1 (or something entirely different: UTF-8 was
                 * chosen as the default, for lack of better alternative),
                 * and may contain byte sequences that will cause a UTF-8
                 * converter to throw exceptions.
		 *
		 * It is safe to use a source encoding of ISO-8859-1 in this
		 * case, as there are no invalid byte sequences in ISO-8859-1,
		 * and the byte/character sequences we're looking for (i.e.,
		 * <jsp:root>) are identical in either encoding (both UTF-8
		 * and ISO-8859-1 are extensions of ASCII).
		 */
		sourceEnc = "ISO-8859-1";
		revert = true;
	    }
	}

	if (isXml) {
	    // (This implies 'isExternal' is TRUE.)
	    // We know we're dealing with a JSP document (via JSP config or
	    // ".jspx" suffix), so we're done.
	    return;
	}

	/*
	 * At this point, 'isExternal' or 'isXml' is FALSE.
	 * Search for jsp:root action, in order to determine if we're dealing
	 * with XML or standard syntax (unless we already know what we're
	 * dealing with, i.e., when 'isExternal' is TRUE and 'isXml' is FALSE).
	 * No check for XML prolog, since nothing prevents a page from
	 * outputting XML and still using JSP syntax (in this case, the
	 * XML prolog is treated as template text).
	 */
	JspReader jspReader = null;
	try {
	    jspReader = new JspReader(ctxt, absFileName, sourceEnc, jarFile,
				      err);
	} catch (FileNotFoundException ex) {
	    throw new JasperException(ex);
	}
        jspReader.setSingleFile(true);
        Mark startMark = jspReader.mark();
	if (!isExternal) {
	    jspReader.reset(startMark);
	    if (hasJspRoot(jspReader)) {
	        isXml = true;
		if (revert) sourceEnc = "UTF-8";
		return;
	    } else {
	        isXml = false;
	    }
	}

	/*
	 * At this point, we know we're dealing with JSP syntax.
	 * If an XML prolog is provided, it's treated as template text.
	 * Determine the page encoding from the page directive, unless it's
	 * specified via JSP config.
	 */
        if (!hasBom) {
            sourceEnc = jspConfigPageEnc;
        }
	if (sourceEnc == null) {
	    sourceEnc = getPageEncodingForJspSyntax(jspReader, startMark);
	    if (sourceEnc == null) {
		// Default to "ISO-8859-1" per JSP spec
		sourceEnc = "ISO-8859-1";
		isDefaultPageEncoding = true;
	    }
	}
    }

    /*
     * Determines page source encoding for page or tag file in JSP syntax,
     * by reading (in this order) the value of the 'pageEncoding' page
     * directive attribute, or the charset value of the 'contentType' page
     * directive attribute.
     *
     * @return The page encoding, or null if not found
     */
    private String getPageEncodingForJspSyntax(JspReader jspReader,
					       Mark startMark)
	        throws JasperException {

	String encoding = null;
        String saveEncoding = null;

        jspReader.reset(startMark);

	/*
	 * Determine page encoding from directive of the form <%@ page %>,
	 * <%@ tag %>, <jsp:directive.page > or <jsp:directive.tag >.
	 */
        while (true) {
            if (jspReader.skipUntil("<") == null) {
                break;
            }
            // If this is a comment, skip until its end
            if (jspReader.matches("%--")) {
                if (jspReader.skipUntil("--%>") == null) {
                    // error will be caught in Parser
                    break;
                }
                continue;
            }
            boolean isDirective = jspReader.matches("%@");
            if (isDirective) {
	        jspReader.skipSpaces();
            }
            else {
                isDirective = jspReader.matches("jsp:directive.");
            }
            if (!isDirective) {
                continue;
            }

	    // compare for "tag ", so we don't match "taglib"
	    if (jspReader.matches("tag ") || jspReader.matches("page")) {

		jspReader.skipSpaces();
                Attributes attrs = Parser.parseAttributes(this, jspReader);
		encoding = getPageEncodingFromDirective(attrs, "pageEncoding");
                if (encoding != null) {
                    break;
                }
		encoding = getPageEncodingFromDirective(attrs, "contentType");
                if (encoding != null) {
                    saveEncoding = encoding;
                }
	    }
	}

        if (encoding == null) {
            encoding = saveEncoding;
        }

	return encoding;
    }

    /*
     * Scans the given attributes for the attribute with the given name,
     * which is either 'pageEncoding' or 'contentType', and returns the
     * specified page encoding.
     *
     * In the case of 'contentType', the page encoding is taken from the
     * content type's 'charset' component.
     *
     * @param attrs The page directive attributes
     * @param attrName The name of the attribute to search for (either
     * 'pageEncoding' or 'contentType')
     *
     * @return The page encoding, or null
     */
    private String getPageEncodingFromDirective(Attributes attrs,
                                                String attrName) {
	String value = attrs.getValue(attrName);
        if (attrName.equals("pageEncoding")) {
            return value;
        }

        // attrName = contentType
        String contentType = value;
        String encoding = null;
        if (contentType != null) {
	    int loc = contentType.indexOf(CHARSET);
	    if (loc != -1) {
		encoding = contentType.substring(loc + CHARSET.length());
	    }
	}

	return encoding;
    }

    /*
     * Resolve the name of the file and update baseDirStack() to keep track of
     * the current base directory for each included file.
     * The 'root' file is always an 'absolute' path, so no need to put an
     * initial value in the baseDirStack.
     */
    private String resolveFileName(String inFileName) {
        String fileName = inFileName.replace('\\', '/');
        boolean isAbsolute = fileName.startsWith("/");
	fileName = isAbsolute ? fileName : baseDirStack.peek() + fileName;
        String baseDir = fileName.substring(0, fileName.lastIndexOf("/") + 1);
	baseDirStack.push(baseDir);
	return fileName;
    }

    /*
     * Checks to see if the given page contains, as its first element, a <root>
     * element whose prefix is bound to the JSP namespace, as in:
     *
     * <wombat:root xmlns:wombat="http://java.sun.com/JSP/Page" version="1.2">
     *   ...
     * </wombat:root>
     *
     * @param reader The reader for this page
     *
     * @return true if this page contains a root element whose prefix is bound
     * to the JSP namespace, and false otherwise
     */
    private boolean hasJspRoot(JspReader reader) throws JasperException {

	// <prefix>:root must be the first element
	Mark start = null;
	while ((start = reader.skipUntil("<")) != null) {
	    int c = reader.nextChar();
	    if (c != '!' && c != '?') break;
	}
	if (start == null) {
	    return false;
	}
	Mark stop = reader.skipUntil(":root");
	if (stop == null) {
	    return false;
	}
	// call substring to get rid of leading '<'
	String prefix = reader.getText(start, stop).substring(1);

	start = stop;
	stop = reader.skipUntil(">");
	if (stop == null) {
	    return false;
	}

	// Determine namespace associated with <root> element's prefix
	String root = reader.getText(start, stop);
	String xmlnsDecl = "xmlns:" + prefix;
	int index = root.indexOf(xmlnsDecl);
	if (index == -1) {
	    return false;
	}
	index += xmlnsDecl.length();
	while (index < root.length()
	           && Character.isWhitespace(root.charAt(index))) {
	    index++;
	}
	if (index < root.length() && root.charAt(index) == '=') {
	    index++;
	    while (index < root.length()
		       && Character.isWhitespace(root.charAt(index))) {
		index++;
	    }
	    if (index < root.length() && root.charAt(index++) == '"'
		    && root.regionMatches(index, JSP_URI, 0,
					  JSP_URI.length())) {
		return true;
	    }
	}

	return false;
    }

    private JarFile getJarFile(URL jarFileUrl) throws IOException {
	JarFile jarFile = null;

	if (jarFileUrl != null) {
	    JarURLConnection conn = (JarURLConnection) jarFileUrl.openConnection();
	    conn.setUseCaches(false);
	    conn.connect();
	    jarFile = conn.getJarFile();
	}

	return jarFile;
    }

}


/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 *
 *
 * This file incorporates work covered by the following copyright and
 * permission notice:
 *
 * Copyright 2004 The Apache Software Foundation
 *
 * Licensed 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.jasper;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.jsp.tagext.TagInfo;
import javax.servlet.jsp.tagext.TagLibraryInfo;

import org.apache.jasper.compiler.Compiler;
import org.apache.jasper.compiler.JspRuntimeContext;
import org.apache.jasper.compiler.JspUtil;
import org.apache.jasper.compiler.Localizer;
import org.apache.jasper.compiler.ServletWriter;
import org.apache.jasper.compiler.TagLibraryInfoImpl;
import org.apache.jasper.servlet.JasperLoader;
import org.apache.jasper.servlet.JspServletWrapper;

/**
 * A place holder for various things that are used through out the JSP
 * engine. This is a per-request/per-context data structure. Some of
 * the instance variables are set at different points.
 *
 * Most of the path-related stuff is here - mangling names, versions, dirs,
 * loading resources and dealing with uris.
 *
 * @author Anil K. Vijendran
 * @author Harish Prabandham
 * @author Pierre Delisle
 * @author Costin Manolache
 * @author Kin-man Chung
 */
public class JspCompilationContext {

    private String className;
    private String jspUri;
    private boolean isErrPage;
    private String basePackageName;
    private String derivedPackageName;
    private String servletJavaFileName;
    private String javaPath;
    private String classFileName;
    private String contentType;
    private ServletWriter writer;
    private Options options;
    private JspServletWrapper jsw;
    private Compiler jspCompiler;
    private String classPath;

    private String baseURI;
    private String outputDir;
    private ServletContext context;
    private ClassLoader loader;

    private JspRuntimeContext rctxt;

    private int removed = 0;

    private URL baseUrl;
    private Class servletClass;

    private boolean isTagFile;
    private boolean protoTypeMode;
    private TagInfo tagInfo;
    private URL tagFileJarUrl;
    private ConcurrentHashMap<String, TagLibraryInfoImpl> taglibs;
    private ConcurrentHashMap<String, URL> tagFileJarUrls;

    // jspURI _must_ be relative to the context
    public JspCompilationContext(String jspUri,
                                 boolean isErrPage,
                                 Options options,
                                 ServletContext context,
                                 JspServletWrapper jsw,
                                 JspRuntimeContext rctxt)
            throws JasperException {

        this.jspUri = canonicalURI(jspUri);
        this.isErrPage = isErrPage;
        this.options = options;
        this.jsw = jsw;
        this.context = context;

        this.baseURI = jspUri.substring(0, jspUri.lastIndexOf('/') + 1);
        // hack fix for resolveRelativeURI
        if (baseURI == null) {
            baseURI = "/";
        } else if (baseURI.charAt(0) != '/') {
            // strip the basde slash since it will be combined with the
            // uriBase to generate a file
            baseURI = "/" + baseURI;
        }
        if (baseURI.charAt(baseURI.length() - 1) != '/') {
            baseURI += '/';
        }

        this.rctxt = rctxt;
        this.basePackageName = Constants.JSP_PACKAGE_NAME;

        taglibs = cast(context.getAttribute(Constants.JSP_TAGLIBRARY_CACHE));
        tagFileJarUrls = cast(context.getAttribute(Constants.JSP_TAGFILE_JAR_URLS_CACHE));
    }

    @SuppressWarnings("unchecked")
    private static <T> T cast(Object x) {
        return (T) x;
    }

    public JspCompilationContext(String tagfile,
                                 TagInfo tagInfo,
                                 Options options,
                                 ServletContext context,
                                 JspServletWrapper jsw,
                                 JspRuntimeContext rctxt,
                                 URL tagFileJarUrl)
            throws JasperException {
        this(tagfile, false, options, context, jsw, rctxt);
        this.isTagFile = true;
        this.tagInfo = tagInfo;
        this.tagFileJarUrl = tagFileJarUrl;
    }

    /* ==================== Methods to override ==================== */

    /**
     * Adds the given tag library with the given URI to the context-wide
     * tag library cache.
     *
     * @param uri The tag library URI
     * @param taglib The tag library to add
     */
    public void addTaglib(String uri, TagLibraryInfoImpl taglib) {
        taglibs.put(uri, taglib);
    }

    /**
     * Gets the context-wide tag library cache.
     *
     * @return The context-wide tag library cache
     */
    public ConcurrentHashMap<String, TagLibraryInfoImpl> getTaglibs() {
        return taglibs;
    }

    /**
     * Clears the context-wide tag library cache.
     */
    public void clearTaglibs() {
        taglibs.clear();
    }

    /**
     * Clears the context-wide mappings from JAR packaged tag file paths to
     * their corresponding URLs.
     */
    public void clearTagFileJarUrls() {
        tagFileJarUrls.clear();
    }

    /** ---------- Class path and loader ---------- */

    /**
     * The classpath that is passed off to the Java compiler.
     */
    public String getClassPath() {
        if( classPath != null )
            return classPath;
        return rctxt.getClassPath();
    }

    /**
     * The classpath that is passed off to the Java compiler.
     */
    public void setClassPath(String classPath) {
        this.classPath = classPath;
    }

    /**
     * What class loader to use for loading classes while compiling
     * this JSP?
     */
    public ClassLoader getClassLoader() {
        if( loader == null )
            loader = rctxt.getParentClassLoader();
        return loader;
    }

    public void setClassLoader(URLClassLoader loader) {
        this.loader = loader;
    }

    /** ---------- Input/Output  ---------- */

    /**
     * The output directory to generate code into.  The output directory
     * is make up of the scratch directory, which is provide in Options,
     * plus the directory derived from the package name.
     */
    public String getOutputDir() {
	if (outputDir == null) {
	    createOutputDir();
	}

        return outputDir;
    }

    /**
     * Create a compiler object for parsing only.
     */
    public Compiler createParser() throws JasperException {
        jspCompiler = new Compiler(this, jsw);
        return jspCompiler;
    }

    /**
     * Create a "Compiler" object.
     */
    public Compiler createCompiler(boolean jspcMode) throws JasperException {
        if (jspCompiler != null ) {
            return jspCompiler;
        }

        jspCompiler = new Compiler(this, jsw, jspcMode);
        return jspCompiler;
    }

    public Compiler getCompiler() {
        return jspCompiler;
    }

    /** ---------- Access resources in the webapp ---------- */

    /**
     * Get the full value of a URI relative to this compilations context
     * uses current file as the base.
     */
    public String resolveRelativeUri(String uri) {
        // sometimes we get uri's massaged from File(String), so check for
        // a root directory deperator char
        if (uri.startsWith("/") || uri.startsWith(File.separator)) {
            return uri;
        } else {
            return baseURI + uri;
        }
    }

    /**
     * Gets a resource as a stream, relative to the meanings of this
     * context's implementation.
     * @return a null if the resource cannot be found or represented
     *         as an InputStream.
     */
    public java.io.InputStream getResourceAsStream(String res)
           throws JasperException {
        return context.getResourceAsStream(canonicalURI(res));
    }

    public URL getResource(String res) throws MalformedURLException {
        URL result = null;

        if (res.startsWith("/META-INF/")) {
            // This is a tag file packaged in a jar that is being compiled
            URL jarUrl = tagFileJarUrls.get(res);
            if (jarUrl == null) {
                jarUrl = tagFileJarUrl;
            }
            if (jarUrl != null) {
                result = new URL(jarUrl.toExternalForm() + res.substring(1));
            }
        } else if (res.startsWith("jar:file:")) {
            // This is a tag file packaged in a jar that is being checked
            // for a dependency
            result = new URL(res);
        } else {
            try {
                result = context.getResource(canonicalURI(res));
            } catch (JasperException ex) {
                throw new MalformedURLException(ex.getMessage());
            }
        }
        return result;
    }

    public Set<String> getResourcePaths(String path)
           throws JasperException {
        return context.getResourcePaths(canonicalURI(path));
    }

    /**
     * Gets the actual path of a URI relative to the context of
     * the compilation.
     */
    public String getRealPath(String path) {
        if (context != null) {
            return context.getRealPath(path);
        }
        return path;
    }

    /**
     * Gets the context-wide mappings from JAR packaged tag file paths to
     * their corresponfing URLs.
     */
    public ConcurrentHashMap<String, URL> getTagFileJarUrls() {
        return this.tagFileJarUrls;
    }

    /**
     * Returns the JAR file in which the tag file for which this
     * JspCompilationContext was created is packaged, or null if this
     * JspCompilationContext does not correspond to a tag file, or if the
     * corresponding tag file is not packaged in a JAR.
     */
    public URL getTagFileJarUrl() {
        return this.tagFileJarUrl;
    }

    /* ==================== Common implementation ==================== */

    /**
     * Just the class name (does not include package name) of the
     * generated class.
     */
    public String getServletClassName() {

        if (className != null) {
            return className;
        }

        if (isTagFile) {
            className = tagInfo.getTagClassName();
            int lastIndex = className.lastIndexOf('.');
            if (lastIndex != -1) {
                className = className.substring(lastIndex + 1);
            }
        } else {
            int iSep = jspUri.lastIndexOf('/') + 1;
            className = JspUtil.makeJavaIdentifier(jspUri.substring(iSep));
        }
        return className;
    }

    public void setServletClassName(String className) {
        this.className = className;
    }

    /**
     * Path of the JSP URI. Note that this is not a file name. This is
     * the context rooted URI of the JSP file.
     */
    public String getJspFile() {
        return jspUri;
    }

    /**
     * Are we processing something that has been declared as an
     * errorpage?
     */
    public boolean isErrorPage() {
        return isErrPage;
    }

    public void setErrorPage(boolean isErrPage) {
        this.isErrPage = isErrPage;
    }

    public boolean isTagFile() {
        return isTagFile;
    }

    public TagInfo getTagInfo() {
        return tagInfo;
    }

    public void setTagInfo(TagInfo tagi) {
        tagInfo = tagi;
    }

    /**
     * True if we are compiling a tag file in prototype mode.
     * ie we only generate codes with class for the tag handler with empty
     * method bodies.
     */
    public boolean isPrototypeMode() {
        return protoTypeMode;
    }

    public void setPrototypeMode(boolean pm) {
        protoTypeMode = pm;
    }

    /**
     * Package name for the generated class is make up of the base package
     * name, which is user settable, and the derived package name.  The
     * derived package name directly mirrors the file heirachy of the JSP page.
     */
    public String getServletPackageName() {
        if (isTagFile()) {
            String className = tagInfo.getTagClassName();
            int lastIndex = className.lastIndexOf('.');
            String pkgName = "";
            if (lastIndex != -1) {
                pkgName = className.substring(0, lastIndex);
            }
            return pkgName;
        } else {
            String dPackageName = getDerivedPackageName();
            if (dPackageName.length() == 0) {
                return basePackageName;
            }
            return basePackageName + '.' + getDerivedPackageName();
        }
    }

    private String getDerivedPackageName() {
	if (derivedPackageName == null) {
            int iSep = jspUri.lastIndexOf('/');
            derivedPackageName = (iSep > 0) ?
                JspUtil.makeJavaPackage(jspUri.substring(1,iSep)) : "";
        }
        return derivedPackageName;
    }

    /**
     * The package name into which the servlet class is generated.
     */
    public void setServletPackageName(String servletPackageName) {
        this.basePackageName = servletPackageName;
    }

    /**
     * Full path name of the Java file into which the servlet is being
     * generated.
     */
    public String getServletJavaFileName() {

        if (servletJavaFileName == null) {
            servletJavaFileName =
		getOutputDir() + getServletClassName() + ".java";
        }
        return servletJavaFileName;
    }

    /**
     * Get hold of the Options object for this context.
     */
    public Options getOptions() {
        return options;
    }

    public ServletContext getServletContext() {
        return context;
    }

    public JspRuntimeContext getRuntimeContext() {
        return rctxt;
    }

    /**
     * Full class name
     */
    public String getFullClassName() {
        if (isTagFile()) {
            return tagInfo.getTagClassName();
        }
        return getServletPackageName() + '.' + getServletClassName();
    }

    /**
     * Path of the Java file relative to the work directory.
     */
    public String getJavaPath() {

        if (javaPath != null) {
            return javaPath;
        }

        javaPath = getFullClassName().replace('.','/') + ".java";
        return javaPath;
    }

    public String getClassFileName() {

        if (classFileName == null) {
            classFileName = getOutputDir() + getServletClassName() + ".class";
        }
        return classFileName;
    }

    /**
     * Get the content type of this JSP.
     *
     * Content type includes content type and encoding.
     */
    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    /**
     * Where is the servlet being generated?
     */
    public ServletWriter getWriter() {
        return writer;
    }

    public void setWriter(ServletWriter writer) {
        this.writer = writer;
    }

    /**
     * Gets the 'location' of the TLD associated with the given taglib 'uri'.
     *
     * @return An array of two Strings: The first element denotes the real
     * path to the TLD. If the path to the TLD points to a jar file, then the
     * second element denotes the name of the TLD entry in the jar file.
     * Returns null if the given uri is not associated with any tag library
     * 'exposed' in the web application.
     */
    public String[] getTldLocation(String uri) throws JasperException {
        String[] location =
            getOptions().getTldScanner().getLocation(uri);
        return location;
    }

    /**
     * Are we keeping generated code around?
     */
    public boolean keepGenerated() {
        return getOptions().getKeepGenerated();
    }

    // ==================== Removal ====================

    public void incrementRemoved() {
        if (removed > 1) {
            jspCompiler.removeGeneratedFiles();
            if( rctxt != null )
                rctxt.removeWrapper(jspUri);
        }
        removed++;
    }

    public boolean isRemoved() {
        if (removed > 1 ) {
            return true;
        }
        return false;
    }

    // ==================== Compile and reload ====================

    public void compile() throws JasperException, FileNotFoundException {
        createCompiler(false);
        if (jspCompiler.isOutDated()) {
            try {
                jspCompiler.compile(true);
                jsw.setReload(true);
                jsw.setCompilationException(null);
            } catch (JasperException ex) {
                // Cache compilation exception
                jsw.setCompilationException(ex);
                throw ex;
            } catch (Exception ex) {
                ex.printStackTrace();
                JasperException je = new JasperException(
                            Localizer.getMessage("jsp.error.unable.compile"),
                            ex);
                // Cache compilation exception
                jsw.setCompilationException(je);
                throw je;
            }
        }
    }

    // ==================== Manipulating the class ====================

    public Class load()
        throws JasperException, ClassNotFoundException
    {
        try {

            String name = getFullClassName();

            if (options.getUsePrecompiled()) {
                servletClass = getClassLoader().loadClass(name);
            } else {
                servletClass = getJspLoader().loadClass(name);
            }
        } catch (ClassNotFoundException cex) {
            // Do not wrapper this in JasperException if use-precompiled is set,
            // because this is really a 404.
            if (options.getUsePrecompiled()) {
                throw cex;
            }
            throw new JasperException(
                    Localizer.getMessage("jsp.error.unable.load"), cex);
        } catch (Exception ex) {
            throw new JasperException(
                    Localizer.getMessage("jsp.error.unable.compile"), ex);
        }
        removed = 0;
        return servletClass;
    }

    public ClassLoader getJspLoader() {
        return new JasperLoader(new URL[] {baseUrl},
                                getClassLoader(),
                                rctxt.getPermissionCollection(),
                                rctxt.getCodeSource(),
                                rctxt.getBytecodes());
    }

    public void makeOutputDir(String outdir) {
        synchronized(outputDirLock) {
            File outDirFile = new File(outdir);
            outDirFile.mkdirs();
            outputDir = outdir;
        }
    }

    // ==================== Private methods ====================

    static Object outputDirLock = new Object();

    private void createOutputDir() {
        String path = null;
        if (isTagFile()) {
	    String tagName = tagInfo.getTagClassName();
            path = tagName.replace('.', '/');
	    path = path.substring(0, path.lastIndexOf('/'));
        } else {
            path = getServletPackageName().replace('.', '/');
	}

        try {
            // Append servlet or tag handler path to scratch dir
            File f = new File( options.getScratchDir(), path );
            makeOutputDir(f.getPath() + File.separator);
            baseUrl = options.getScratchDir().toURL();
        } catch (Exception e) {
            throw new IllegalStateException("No output directory: " +
                                            e.getMessage());
        }
    }

    private static final boolean isPathSeparator(char c) {
       return (c == '/' || c == '\\');
    }

    private static final String canonicalURI(String s)
           throws JasperException {
       if (s == null) return null;
       StringBuilder result = new StringBuilder();
       final int len = s.length();
       int pos = 0;
       while (pos < len) {
           char c = s.charAt(pos);
           if ( isPathSeparator(c) ) {
               /*
                * multiple path separators.
                * 'foo///bar' -> 'foo/bar'
                */
               while (pos+1 < len && isPathSeparator(s.charAt(pos+1))) {
                   ++pos;
               }

               if (pos+1 < len && s.charAt(pos+1) == '.') {
                   /*
                    * a single dot at the end of the path - we are done.
                    */
                   if (pos+2 >= len) break;

                   switch (s.charAt(pos+2)) {
                       /*
                        * self directory in path
                        * foo/./bar -> foo/bar
                        */
                   case '/':
                   case '\\':
                       pos += 2;
                       continue;

                       /*
                        * two dots in a path: go back one hierarchy.
                        * foo/bar/../baz -> foo/baz
                        */
                   case '.':
                       // only if we have exactly _two_ dots.
                       if (pos+3 < len && isPathSeparator(s.charAt(pos+3))) {
                           pos += 3;
                           int separatorPos = result.length()-1;
                           if (separatorPos < 0) {
                               throw new JasperException(
                                   Localizer.getMessage("jsp.error.badpath",s));
                           }
                           while (separatorPos >= 0 &&
                                  ! isPathSeparator(result
                                                    .charAt(separatorPos))) {
                               --separatorPos;
                           }
                           if (separatorPos >= 0)
                               result.setLength(separatorPos);
                           continue;
                       }
                   }
               }
           }
           result.append(c);
           ++pos;
       }
       return result.toString();
    }
}


/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 *
 *
 * This file incorporates work covered by the following copyright and
 * permission notice:
 *
 * Copyright 2004 The Apache Software Foundation
 *
 * Licensed 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.jasper.xmlparser;

import java.io.InputStream;
import java.io.IOException;
import java.io.Reader;
import org.apache.jasper.compiler.Localizer;

/**
 * A simple ASCII byte reader. This is an optimized reader for reading
 * byte streams that only contain 7-bit ASCII characters.
 *
 * @author Andy Clark, IBM
 *
 * @version $Id: ASCIIReader.java,v 1.3 2006/12/12 02:12:48 jluehe Exp $
 */
public class ASCIIReader
    extends Reader {

    //
    // Constants
    //

    /** Default byte buffer size (2048). */
    public static final int DEFAULT_BUFFER_SIZE = 2048;

    //
    // Data
    //

    /** Input stream. */
    protected InputStream fInputStream;

    /** Byte buffer. */
    protected byte[] fBuffer;

    //
    // Constructors
    //

    /**
     * Constructs an ASCII reader from the specified input stream
     * and buffer size.
     *
     * @param inputStream The input stream.
     * @param size        The initial buffer size.
     */
    public ASCIIReader(InputStream inputStream, int size) {
        fInputStream = inputStream;
        fBuffer = new byte[size];
    }

    //
    // Reader methods
    //

    /**
     * Read a single character.  This method will block until a character is
     * available, an I/O error occurs, or the end of the stream is reached.
     *
     * <p> Subclasses that intend to support efficient single-character input
     * should override this method.
     *
     * @return     The character read, as an integer in the range 0 to 127
     *             (<tt>0x00-0x7f</tt>), or -1 if the end of the stream has
     *             been reached
     *
     * @exception  IOException  If an I/O error occurs
     */
    public int read() throws IOException {
        int b0 = fInputStream.read();
        if (b0 > 0x80) {
            throw new IOException(Localizer.getMessage("jsp.error.xml.invalidASCII",
						       Integer.toString(b0)));
        }
        return b0;
    } // read():int

    /**
     * Read characters into a portion of an array.  This method will block
     * until some input is available, an I/O error occurs, or the end of the
     * stream is reached.
     *
     * @param      ch     Destination buffer
     * @param      offset Offset at which to start storing characters
     * @param      length Maximum number of characters to read
     *
     * @return     The number of characters read, or -1 if the end of the
     *             stream has been reached
     *
     * @exception  IOException  If an I/O error occurs
     */
    public int read(char ch[], int offset, int length) throws IOException {
        if (length > fBuffer.length) {
            length = fBuffer.length;
        }
        int count = fInputStream.read(fBuffer, 0, length);
        for (int i = 0; i < count; i++) {
            int b0 = fBuffer[i];
            if (b0 < 0) {
                throw new IOException(Localizer.getMessage("jsp.error.xml.invalidASCII",
							   Integer.toString(b0)));
            }
            ch[offset + i] = (char)b0;
        }
        return count;
    } // read(char[],int,int)

    /**
     * Skip characters.  This method will block until some characters are
     * available, an I/O error occurs, or the end of the stream is reached.
     *
     * @param  n  The number of characters to skip
     *
     * @return    The number of characters actually skipped
     *
     * @exception  IOException  If an I/O error occurs
     */
    public long skip(long n) throws IOException {
        return fInputStream.skip(n);
    } // skip(long):long

    /**
     * Tell whether this stream is ready to be read.
     *
     * @return True if the next read() is guaranteed not to block for input,
     * false otherwise.  Note that returning false does not guarantee that the
     * next read will block.
     *
     * @exception  IOException  If an I/O error occurs
     */
    public boolean ready() throws IOException {
	return false;
    } // ready()

    /**
     * Tell whether this stream supports the mark() operation.
     */
    public boolean markSupported() {
	return fInputStream.markSupported();
    } // markSupported()

    /**
     * Mark the present position in the stream.  Subsequent calls to reset()
     * will attempt to reposition the stream to this point.  Not all
     * character-input streams support the mark() operation.
     *
     * @param  readAheadLimit  Limit on the number of characters that may be
     *                         read while still preserving the mark.  After
     *                         reading this many characters, attempting to
     *                         reset the stream may fail.
     *
     * @exception  IOException  If the stream does not support mark(),
     *                          or if some other I/O error occurs
     */
    public void mark(int readAheadLimit) throws IOException {
	fInputStream.mark(readAheadLimit);
    } // mark(int)

    /**
     * Reset the stream.  If the stream has been marked, then attempt to
     * reposition it at the mark.  If the stream has not been marked, then
     * attempt to reset it in some way appropriate to the particular stream,
     * for example by repositioning it to its starting point.  Not all
     * character-input streams support the reset() operation, and some support
     * reset() without supporting mark().
     *
     * @exception  IOException  If the stream has not been marked,
     *                          or if the mark has been invalidated,
     *                          or if the stream does not support reset(),
     *                          or if some other I/O error occurs
     */
    public void reset() throws IOException {
        fInputStream.reset();
    } // reset()

    /**
     * Close the stream.  Once a stream has been closed, further read(),
     * ready(), mark(), or reset() invocations will throw an IOException.
     * Closing a previously-closed stream, however, has no effect.
     *
     * @exception  IOException  If an I/O error occurs
     */
     public void close() throws IOException {
         fInputStream.close();
     } // close()

} // class ASCIIReader


/*******************************************************************************
 * Copyright (c) 2015 IBH SYSTEMS GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.repo.osgi.web;

import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;

import java.time.Instant;
import java.util.Optional;

import org.eclipse.packagedrone.repo.channel.ChannelInformation;
import org.eclipse.packagedrone.repo.channel.ChannelService;
import org.eclipse.packagedrone.repo.web.sitemap.ChangeFrequency;
import org.eclipse.packagedrone.repo.web.sitemap.SitemapGenerator;
import org.eclipse.packagedrone.repo.web.sitemap.SitemapIndexContext;
import org.eclipse.packagedrone.repo.web.sitemap.UrlSetContext;
import org.eclipse.packagedrone.repo.web.sitemap.UrlSetContextCreator;

public class OsgiSitemapGenerator implements SitemapGenerator
{
    private ChannelService channelService;

    public void setChannelService ( final ChannelService channelService )
    {
        this.channelService = channelService;
    }

    @Override
    public void gatherRoots ( final SitemapIndexContext context )
    {
        context.addLocation ( "channels_osgi", calcLastMod () );
    }

    @Override
    public void render ( final String path, final UrlSetContextCreator creator )
    {
        final String[] toks = path.split ( "/" );

        if ( toks.length == 1 && "channels_osgi".equals ( toks[0] ) )
        {
            // write out url set of all channels

            final UrlSetContext context = creator.createUrlSet ();

            for ( final ChannelInformation ci : this.channelService.list () )
            {
                if ( !ci.hasAspect ( "osgi" ) )
                {
                    continue;
                }

                final Optional<Instant> lastMod = ofNullable ( ci.getState ().getModificationTimestamp () );
                final String id = urlPathSegmentEscaper ().escape ( ci.getId () );

                context.addLocation ( String.format ( "/osgi.info/channel/%s/infoBundles", id ), lastMod, of ( ChangeFrequency.DAILY ), empty () );
                context.addLocation ( String.format ( "/osgi.info/channel/%s/infoFeatures", id ), lastMod, of ( ChangeFrequency.DAILY ), empty () );
            }
        }
    }

    /**
     * Find the last modification timestamp of all channels
     *
     * @return the latest modification timestamp of all channels
     */
    private Optional<Instant> calcLastMod ()
    {
        Instant globalLastMod = null;

        for ( final ChannelInformation ci : this.channelService.list () )
        {
            final Optional<Instant> lastMod = ofNullable ( ci.getState ().getModificationTimestamp () );

            if ( globalLastMod == null || lastMod.get ().isAfter ( globalLastMod ) )
            {
                globalLastMod = lastMod.get ();
            }
        }
        return Optional.ofNullable ( globalLastMod );
    }

}


/*******************************************************************************
 * Copyright (c) 2005-2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.jsp.jasper;

import java.io.IOException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;

// This class is a slightly augmented copy of org.eclipse.core.runtime.internal.adaptor.ContextFinder
// in particular basicFindClassLoaders has been altered to use PackageAdmin to determine if a class originated from
// a bundle and to skip over the various JspClassloader classes.
public class JSPContextFinder extends ClassLoader implements PrivilegedAction {
	static final class Finder extends SecurityManager {
		public Class[] getClassContext() {
			return super.getClassContext();
		}
	}
	//This is used to detect cycle that could be caused while delegating the loading to other classloaders
	//It keeps track on a thread basis of the set of requested classes and resources
	private static ThreadLocal cycleDetector = new ThreadLocal();
	static Finder contextFinder;
	static {
		AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
				contextFinder = new Finder();
				return null;
			}
		});
	}

	public JSPContextFinder(ClassLoader contextClassLoader) {
		super(contextClassLoader);
	}

	// Return a list of all classloaders on the stack that are neither the
	// JSPContextFinder classloader nor the boot classloader.  The last classloader
	// in the list is either a bundle classloader or the framework's classloader
	// We assume that the bootclassloader never uses the context classloader to find classes in itself.
	ArrayList basicFindClassLoaders() {
		Class[] stack = contextFinder.getClassContext();
		ArrayList result = new ArrayList(1);
		ClassLoader previousLoader = null;
		for (int i = 1; i < stack.length; i++) {
			ClassLoader tmp = stack[i].getClassLoader();
			if (checkClass(stack[i]) && tmp != null && tmp != this) {
				if (checkClassLoader(tmp)) {
					if (previousLoader != tmp) {
						result.add(tmp);
						previousLoader = tmp;
					}
				}
				// stop at the framework classloader or the first bundle classloader
				if (Activator.getBundle(stack[i]) != null)
					break;
			}
		}
		return result;
	}

	private boolean checkClass(Class clazz) {
		return clazz != JSPContextFinder.class &&
			clazz != BundleProxyClassLoader.class &&
			clazz != JspClassLoader.class;
	}

	// ensures that a classloader does not have the JSPContextFinder as part of the
	// parent hierachy.  A classloader which has the JSPContextFinder as a parent must
	// not be used as a delegate, otherwise we endup in endless recursion.
	private boolean checkClassLoader(ClassLoader classloader) {
		if (classloader == null || classloader == getParent())
			return false;
		for (ClassLoader parent = classloader.getParent(); parent != null; parent = parent.getParent())
			if (parent == this)
				return false;
		return true;
	}

	private ArrayList findClassLoaders() {
		if (System.getSecurityManager() == null)
			return basicFindClassLoaders();
		return (ArrayList) AccessController.doPrivileged(this);
	}

	public Object run() {
		return basicFindClassLoaders();
	}

	//Return whether the request for loading "name" should proceed.
	//False is returned when a cycle is being detected
	private boolean startLoading(String name) {
		Set classesAndResources = (Set) cycleDetector.get();
		if (classesAndResources != null && classesAndResources.contains(name))
			return false;

		if (classesAndResources == null) {
			classesAndResources = new HashSet(3);
			cycleDetector.set(classesAndResources);
		}
		classesAndResources.add(name);
		return true;
	}

	private void stopLoading(String name) {
		((Set) cycleDetector.get()).remove(name);
	}

	protected Class loadClass(String arg0, boolean arg1) throws ClassNotFoundException {
		//Shortcut cycle
		if (startLoading(arg0) == false)
			throw new ClassNotFoundException(arg0);

		try {
			ArrayList toConsult = findClassLoaders();
			for (Iterator loaders = toConsult.iterator(); loaders.hasNext();)
				try {
					return ((ClassLoader) loaders.next()).loadClass(arg0);
				} catch (ClassNotFoundException e) {
					// go to the next class loader
				}
			return super.loadClass(arg0, arg1);
		} finally {
			stopLoading(arg0);
		}
	}

	public URL getResource(String arg0) {
		//Shortcut cycle
		if (startLoading(arg0) == false)
			return null;
		try {
			ArrayList toConsult = findClassLoaders();
			for (Iterator loaders = toConsult.iterator(); loaders.hasNext();) {
				URL result = ((ClassLoader) loaders.next()).getResource(arg0);
				if (result != null)
					return result;
				// go to the next class loader
			}
			return super.getResource(arg0);
		} finally {
			stopLoading(arg0);
		}
	}

	protected Enumeration findResources(String arg0) throws IOException {
		//Shortcut cycle
		if (startLoading(arg0) == false)
			return null;
		try {
			ArrayList toConsult = findClassLoaders();
			for (Iterator loaders = toConsult.iterator(); loaders.hasNext();) {
				Enumeration result = ((ClassLoader) loaders.next()).getResources(arg0);
				if (result != null && result.hasMoreElements())
					return result;
				// go to the next class loader
			}
			return super.findResources(arg0);
		} finally {
			stopLoading(arg0);
		}
	}
}


/*
 * SingleXZInputStream
 *
 * Author: Lasse Collin <lasse.collin@tukaani.org>
 *
 * This file has been put into the public domain.
 * You can do whatever you want with this file.
 */

package org.tukaani.xz;

import java.io.InputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.EOFException;
import org.tukaani.xz.common.DecoderUtil;
import org.tukaani.xz.common.StreamFlags;
import org.tukaani.xz.index.IndexHash;
import org.tukaani.xz.check.Check;

/**
 * Decompresses exactly one XZ Stream in streamed mode (no seeking).
 * The decompression stops after the first XZ Stream has been decompressed,
 * and the read position in the input stream is left at the first byte
 * after the end of the XZ Stream. This can be useful when XZ data has
 * been stored inside some other file format or protocol.
 *

 * Unless you know what you are doing, don't use this class to decompress
 * standalone .xz files. For that purpose, use XZInputStream.
 *
 *
When uncompressed size is known beforehand

 *

 * If you are decompressing complete XZ streams and your application knows
 * exactly how much uncompressed data there should be, it is good to try
 * reading one more byte by calling read() and checking
 * that it returns -1. This way the decompressor will parse the
 * file footers and verify the integrity checks, giving the caller more
 * confidence that the uncompressed data is valid.
 *
 * @see XZInputStream
 */
public class SingleXZInputStream extends InputStream {
    private InputStream in;
    private final ArrayCache arrayCache;
    private final int memoryLimit;
    private final StreamFlags streamHeaderFlags;
    private final Check check;
    private final boolean verifyCheck;
    private BlockInputStream blockDecoder = null;
    private final IndexHash indexHash = new IndexHash();
    private boolean endReached = false;
    private IOException exception = null;

    private final byte[] tempBuf = new byte[1];

    /**
     * Reads the Stream Header into a buffer.
     * This is a helper function for the constructors.
     */
    private static byte[] readStreamHeader(InputStream in) throws IOException {
        byte[] streamHeader = new byte[DecoderUtil.STREAM_HEADER_SIZE];
        new DataInputStream(in).readFully(streamHeader);
        return streamHeader;
    }

    /**
     * Creates a new XZ decompressor that decompresses exactly one
     * XZ Stream from in without a memory usage limit.
     *

     * This constructor reads and parses the XZ Stream Header (12 bytes)
     * from in. The header of the first Block is not read
     * until read is called.
     *
     * @param       in          input stream from which XZ-compressed
     *                          data is read
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ header CRC32 doesn't match
     *
     * @throws      UnsupportedOptionsException
     *                          XZ header is valid but specifies options
     *                          not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 12 bytes of input was available
     *                          from in
     *
     * @throws      IOException may be thrown by in
     */
    public SingleXZInputStream(InputStream in) throws IOException {
        this(in, -1);
    }

    /**
     * Creates a new XZ decompressor that decompresses exactly one
     * XZ Stream from in without a memory usage limit.
     *

     * This is identical to SingleXZInputStream(InputStream)
     * except that this also takes the arrayCache argument.
     *
     * @param       in          input stream from which XZ-compressed
     *                          data is read
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ header CRC32 doesn't match
     *
     * @throws      UnsupportedOptionsException
     *                          XZ header is valid but specifies options
     *                          not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 12 bytes of input was available
     *                          from in
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.7
     */
    public SingleXZInputStream(InputStream in, ArrayCache arrayCache)
            throws IOException {
        this(in, -1, arrayCache);
    }

    /**
     * Creates a new XZ decompressor that decompresses exactly one
     * XZ Stream from in with an optional memory usage limit.
     *

     * This is identical to SingleXZInputStream(InputStream)
     * except that this also takes the memoryLimit argument.
     *
     * @param       in          input stream from which XZ-compressed
     *                          data is read
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ header CRC32 doesn't match
     *
     * @throws      UnsupportedOptionsException
     *                          XZ header is valid but specifies options
     *                          not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 12 bytes of input was available
     *                          from in
     *
     * @throws      IOException may be thrown by in
     */
    public SingleXZInputStream(InputStream in, int memoryLimit)
            throws IOException {
        this(in, memoryLimit, true);
    }

    /**
     * Creates a new XZ decompressor that decompresses exactly one
     * XZ Stream from in with an optional memory usage limit.
     *

     * This is identical to SingleXZInputStream(InputStream)
     * except that this also takes the memoryLimit and
     * arrayCache arguments.
     *
     * @param       in          input stream from which XZ-compressed
     *                          data is read
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ header CRC32 doesn't match
     *
     * @throws      UnsupportedOptionsException
     *                          XZ header is valid but specifies options
     *                          not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 12 bytes of input was available
     *                          from in
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.7
     */
    public SingleXZInputStream(InputStream in, int memoryLimit,
                               ArrayCache arrayCache) throws IOException {
        this(in, memoryLimit, true, arrayCache);
    }

    /**
     * Creates a new XZ decompressor that decompresses exactly one
     * XZ Stream from in with an optional memory usage limit
     * and ability to disable verification of integrity checks.
     *

     * This is identical to SingleXZInputStream(InputStream,int)
     * except that this also takes the verifyCheck argument.
     *

     * Note that integrity check verification should almost never be disabled.
     * Possible reasons to disable integrity check verification:
     *

     *
Trying to recover data from a corrupt .xz file.

     *
Speeding up decompression. This matters mostly with SHA-256
     *   or with files that have compressed extremely well. It's recommended
     *   that integrity checking isn't disabled for performance reasons
     *   unless the file integrity is verified externally in some other
     *   way.

     *

     *

     * verifyCheck only affects the integrity check of
     * the actual compressed data. The CRC32 fields in the headers
     * are always verified.
     *
     * @param       in          input stream from which XZ-compressed
     *                          data is read
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @param       verifyCheck if true, the integrity checks
     *                          will be verified; this should almost never
     *                          be set to false
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ header CRC32 doesn't match
     *
     * @throws      UnsupportedOptionsException
     *                          XZ header is valid but specifies options
     *                          not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 12 bytes of input was available
     *                          from in
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.6
     */
    public SingleXZInputStream(InputStream in, int memoryLimit,
                               boolean verifyCheck) throws IOException {
        this(in, memoryLimit, verifyCheck, ArrayCache.getDefaultCache());
    }

    /**
     * Creates a new XZ decompressor that decompresses exactly one
     * XZ Stream from in with an optional memory usage limit
     * and ability to disable verification of integrity checks.
     *

     * This is identical to
     * SingleXZInputStream(InputStream,int,boolean)
     * except that this also takes the arrayCache argument.
     *
     * @param       in          input stream from which XZ-compressed
     *                          data is read
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @param       verifyCheck if true, the integrity checks
     *                          will be verified; this should almost never
     *                          be set to false
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ header CRC32 doesn't match
     *
     * @throws      UnsupportedOptionsException
     *                          XZ header is valid but specifies options
     *                          not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 12 bytes of input was available
     *                          from in
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.7
     */
    public SingleXZInputStream(InputStream in, int memoryLimit,
                               boolean verifyCheck, ArrayCache arrayCache)
            throws IOException {
        this(in, memoryLimit, verifyCheck, readStreamHeader(in), arrayCache);
    }

    SingleXZInputStream(InputStream in, int memoryLimit, boolean verifyCheck,
                        byte[] streamHeader, ArrayCache arrayCache)
            throws IOException {
        this.arrayCache = arrayCache;
        this.in = in;
        this.memoryLimit = memoryLimit;
        this.verifyCheck = verifyCheck;
        streamHeaderFlags = DecoderUtil.decodeStreamHeader(streamHeader);
        check = Check.getInstance(streamHeaderFlags.checkType);
    }

    /**
     * Gets the ID of the integrity check used in this XZ Stream.
     *
     * @return      the Check ID specified in the XZ Stream Header
     */
    public int getCheckType() {
        return streamHeaderFlags.checkType;
    }

    /**
     * Gets the name of the integrity check used in this XZ Stream.
     *
     * @return      the name of the check specified in the XZ Stream Header
     */
    public String getCheckName() {
        return check.getName();
    }

    /**
     * Decompresses the next byte from this input stream.
     *

     * Reading lots of data with read() from this input stream
     * may be inefficient. Wrap it in {@link java.io.BufferedInputStream}
     * if you need to read lots of data one byte at a time.
     *
     * @return      the next decompressed byte, or -1
     *              to indicate the end of the compressed stream
     *
     * @throws      CorruptedInputException
     * @throws      UnsupportedOptionsException
     * @throws      MemoryLimitException
     *
     * @throws      XZIOException if the stream has been closed
     *
     * @throws      EOFException
     *                          compressed input is truncated or corrupt
     *
     * @throws      IOException may be thrown by in
     */
    public int read() throws IOException {
        return read(tempBuf, 0, 1) == -1 ? -1 : (tempBuf[0] & 0xFF);
    }

    /**
     * Decompresses into an array of bytes.
     *

     * If len is zero, no bytes are read and 0
     * is returned. Otherwise this will try to decompress len
     * bytes of uncompressed data. Less than len bytes may
     * be read only in the following situations:
     *

     *
The end of the compressed data was reached successfully.

     *
An error is detected after at least one but less len
     *       bytes have already been successfully decompressed.
     *       The next call with non-zero len will immediately
     *       throw the pending exception.

     *
An exception is thrown.

     *

     *
     * @param       buf         target buffer for uncompressed data
     * @param       off         start offset in buf
     * @param       len         maximum number of uncompressed bytes to read
     *
     * @return      number of bytes read, or -1 to indicate
     *              the end of the compressed stream
     *
     * @throws      CorruptedInputException
     * @throws      UnsupportedOptionsException
     * @throws      MemoryLimitException
     *
     * @throws      XZIOException if the stream has been closed
     *
     * @throws      EOFException
     *                          compressed input is truncated or corrupt
     *
     * @throws      IOException may be thrown by in
     */
    public int read(byte[] buf, int off, int len) throws IOException {
        if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length)
            throw new IndexOutOfBoundsException();

        if (len == 0)
            return 0;

        if (in == null)
            throw new XZIOException("Stream closed");

        if (exception != null)
            throw exception;

        if (endReached)
            return -1;

        int size = 0;

        try {
            while (len > 0) {
                if (blockDecoder == null) {
                    try {
                        blockDecoder = new BlockInputStream(
                                in, check, verifyCheck, memoryLimit, -1, -1,
                                arrayCache);
                    } catch (IndexIndicatorException e) {
                        indexHash.validate(in);
                        validateStreamFooter();
                        endReached = true;
                        return size > 0 ? size : -1;
                    }
                }

                int ret = blockDecoder.read(buf, off, len);

                if (ret > 0) {
                    size += ret;
                    off += ret;
                    len -= ret;
                } else if (ret == -1) {
                    indexHash.add(blockDecoder.getUnpaddedSize(),
                                  blockDecoder.getUncompressedSize());
                    blockDecoder = null;
                }
            }
        } catch (IOException e) {
            exception = e;
            if (size == 0)
                throw e;
        }

        return size;
    }

    private void validateStreamFooter() throws IOException {
        byte[] buf = new byte[DecoderUtil.STREAM_HEADER_SIZE];
        new DataInputStream(in).readFully(buf);
        StreamFlags streamFooterFlags = DecoderUtil.decodeStreamFooter(buf);

        if (!DecoderUtil.areStreamFlagsEqual(streamHeaderFlags,
                                             streamFooterFlags)
                || indexHash.getIndexSize() != streamFooterFlags.backwardSize)
            throw new CorruptedInputException(
                    "XZ Stream Footer does not match Stream Header");
    }

    /**
     * Returns the number of uncompressed bytes that can be read
     * without blocking. The value is returned with an assumption
     * that the compressed input data will be valid. If the compressed
     * data is corrupt, CorruptedInputException may get
     * thrown before the number of bytes claimed to be available have
     * been read from this input stream.
     *
     * @return      the number of uncompressed bytes that can be read
     *              without blocking
     */
    public int available() throws IOException {
        if (in == null)
            throw new XZIOException("Stream closed");

        if (exception != null)
            throw exception;

        return blockDecoder == null ? 0 : blockDecoder.available();
    }

    /**
     * Closes the stream and calls in.close().
     * If the stream was already closed, this does nothing.
     *

     * This is equivalent to close(true).
     *
     * @throws  IOException if thrown by in.close()
     */
    public void close() throws IOException {
        close(true);
    }

    /**
     * Closes the stream and optionally calls in.close().
     * If the stream was already closed, this does nothing.
     * If close(false) has been called, a further
     * call of close(true) does nothing (it doesn't call
     * in.close()).
     *

     * If you don't want to close the underlying InputStream,
     * there is usually no need to worry about closing this stream either;
     * it's fine to do nothing and let the garbage collector handle it.
     * However, if you are using {@link ArrayCache}, close(false)
     * can be useful to put the allocated arrays back to the cache without
     * closing the underlying InputStream.
     *

     * Note that if you successfully reach the end of the stream
     * (read returns -1), the arrays are
     * automatically put back to the cache by that read call. In
     * this situation close(false) is redundant (but harmless).
     *
     * @throws  IOException if thrown by in.close()
     *
     * @since 1.7
     */
    public void close(boolean closeInput) throws IOException {
        if (in != null) {
            if (blockDecoder != null) {
                blockDecoder.close();
                blockDecoder = null;
            }

            try {
                if (closeInput)
                    in.close();
            } finally {
                in = null;
            }
        }
    }
}


/*******************************************************************************
 * Copyright (c) 2015 IBH SYSTEMS GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.web.common.page;

import java.util.Collections;
import java.util.List;

public final class Pagination
{
    private Pagination ()
    {
    }

    @FunctionalInterface
    public interface Provider<T>
    {
        public List<T> list ( int startIndex, int length );
    }

    public static <T> PaginationResult<T> paginate ( Integer startPage, final int pageSize, final Provider<T> provider )
    {
        if ( pageSize <= 0 )
        {
            throw new IllegalArgumentException ( String.format ( "Page size must be greater than zero" ) );
        }

        if ( startPage == null || startPage < 0 )
        {
            startPage = 0;
        }

        final int start = startPage * pageSize;

        List<T> data = provider.list ( start, pageSize + 1 ); // request one more than necessary to peek for a next page

        // check for trailing entries
        final boolean hasNext = data.size () > pageSize;

        // now remove the trailing entries
        if ( data.size () > pageSize )
        {
            data = data.subList ( 0, pageSize );
        }

        return new PaginationResult<> ( data, start > 0, hasNext, startPage, pageSize );
    }

    /**
     * Paginate from a full data set
     *
     * @param startPage
     *            the start page
     * @param pageSize
     *            the size of a single page
     * @param fullDataSet
     *            the full data set
     * @return a subset of the full data set, containing the items which match
     *         the page window
     */
    public static <T> PaginationResult<T> paginate ( final Integer startPage, final int pageSize, final List<T> fullDataSet )
    {
        return paginate ( startPage, pageSize, ( start, length ) -> {

            final int len = fullDataSet.size ();
            if ( start > len )
            {
                return Collections.emptyList ();
            }

            return fullDataSet.subList ( start, Math.min ( start + length, len ) );
        } );
    }
}


/*
 * SeekableFileInputStream
 *
 * Author: Lasse Collin <lasse.collin@tukaani.org>
 *
 * This file has been put into the public domain.
 * You can do whatever you want with this file.
 */

package org.tukaani.xz;

import java.io.File;
import java.io.RandomAccessFile;
import java.io.IOException;
import java.io.FileNotFoundException;

/**
 * Wraps a {@link java.io.RandomAccessFile RandomAccessFile}
 * in a SeekableInputStream.
 */
public class SeekableFileInputStream extends SeekableInputStream {
    /**
     * The RandomAccessFile that has been wrapped
     * into a SeekableFileInputStream.
     */
    protected RandomAccessFile randomAccessFile;

    /**
     * Creates a new seekable input stream that reads from the specified file.
     */
    public SeekableFileInputStream(File file) throws FileNotFoundException {
        randomAccessFile = new RandomAccessFile(file, "r");
    }

    /**
     * Creates a new seekable input stream that reads from a file with
     * the specified name.
     */
    public SeekableFileInputStream(String name) throws FileNotFoundException {
        randomAccessFile = new RandomAccessFile(name, "r");
    }

    /**
     * Creates a new seekable input stream from an existing
     * RandomAccessFile object.
     */
    public SeekableFileInputStream(RandomAccessFile randomAccessFile) {
        this.randomAccessFile = randomAccessFile;
    }

    /**
     * Calls {@link RandomAccessFile#read() randomAccessFile.read()}.
     */
    public int read() throws IOException {
        return randomAccessFile.read();
    }

    /**
     * Calls {@link RandomAccessFile#read(byte[]) randomAccessFile.read(buf)}.
     */
    public int read(byte[] buf) throws IOException {
        return randomAccessFile.read(buf);
    }

    /**
     * Calls
     * {@link RandomAccessFile#read(byte[],int,int)
     *        randomAccessFile.read(buf, off, len)}.
     */
    public int read(byte[] buf, int off, int len) throws IOException {
        return randomAccessFile.read(buf, off, len);
    }

    /**
     * Calls {@link RandomAccessFile#close() randomAccessFile.close()}.
     */
    public void close() throws IOException {
        randomAccessFile.close();
    }

    /**
     * Calls {@link RandomAccessFile#length() randomAccessFile.length()}.
     */
    public long length() throws IOException {
        return randomAccessFile.length();
    }

    /**
     * Calls {@link RandomAccessFile#getFilePointer()
                    randomAccessFile.getFilePointer()}.
     */
    public long position() throws IOException {
        return randomAccessFile.getFilePointer();
    }

    /**
     * Calls {@link RandomAccessFile#seek(long) randomAccessFile.seek(long)}.
     */
    public void seek(long pos) throws IOException {
        randomAccessFile.seek(pos);
    }
}


/*******************************************************************************
 * Copyright (c) 2014, 2015 IBH SYSTEMS GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *     M-Ezzat - code cleanup - squid:S2162
 *******************************************************************************/
package org.eclipse.packagedrone.repo;

import java.io.Serializable;

import org.eclipse.packagedrone.utils.converter.ConvertBy;

/**
 * A meta data key
 *

 * A meta data key consists of a namespace and a key part. The namespace is in
 * most cases the ID of the channel aspect factory.
 *

 *

 * Meta keys with the same namespace and key are equal. The natural sort order
 * is first by namespace and then by key. Both compared as strings.
 *

 */
@ConvertBy ( StringToMetaKeyConverter.class )
public class MetaKey implements Comparable<MetaKey>, Serializable
{
    private static final long serialVersionUID = 1L;

    private final String namespace;

    private final String key;

    public MetaKey ( final String namespace, final String key )
    {
        if ( namespace.contains ( ":" ) )
        {
            throw new IllegalArgumentException ( String.format ( "Namespace must not contain ':'" ) );
        }

        this.namespace = namespace;
        this.key = key;
    }

    private MetaKey ( final String namespace, final String key, final boolean checked )
    {
        this.namespace = namespace;
        this.key = key;
    }

    public String getKey ()
    {
        return this.key;
    }

    public String getNamespace ()
    {
        return this.namespace;
    }

    @Override
    public int hashCode ()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + ( this.key == null ? 0 : this.key.hashCode () );
        result = prime * result + ( this.namespace == null ? 0 : this.namespace.hashCode () );
        return result;
    }

    @Override
    public boolean equals ( final Object obj )
    {
        if ( this == obj )
        {
            return true;
        }
        if ( obj == null )
        {
            return false;
        }
        if ( this.getClass() != obj.getClass() )
        {
            return false;
        }
        final MetaKey other = (MetaKey)obj;
        if ( this.key == null )
        {
            if ( other.key != null )
            {
                return false;
            }
        }
        else if ( !this.key.equals ( other.key ) )
        {
            return false;
        }
        if ( this.namespace == null )
        {
            if ( other.namespace != null )
            {
                return false;
            }
        }
        else if ( !this.namespace.equals ( other.namespace ) )
        {
            return false;
        }
        return true;
    }

    @Override
    public int compareTo ( final MetaKey o )
    {
        int rc;

        rc = this.namespace.compareTo ( o.namespace );
        if ( rc != 0 )
        {
            return rc;
        }

        return this.key.compareTo ( o.key );
    }

    @Override
    public String toString ()
    {
        return this.namespace + ":" + this.key;
    }

    /**
     * Convert a string to a MetaKey if possible
     *
     * @param string
     *            the string to convert
     * @return the new MetaKey instance or null if the format was
     *         invalid
     */
    public static MetaKey fromString ( final String string )
    {
        final int idx = string.indexOf ( ':' );
        if ( idx < 1 )
        {
            // -1: none at all, 0: empty namespace
            return null;
        }

        if ( idx + 1 >= string.length () )
        {
            // empty key segment
            return null;
        }

        return new MetaKey ( string.substring ( 0, idx ), string.substring ( idx + 1 ), true );
    }
}