/*
 * 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.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import java.util.logging.Level;

import javax.servlet.ServletContext;
import javax.servlet.descriptor.JspConfigDescriptor;
import javax.servlet.descriptor.JspPropertyGroupDescriptor;

import org.apache.jasper.JasperException;
import org.apache.jasper.Constants;

/**
 * Handles the jsp-config element in WEB_INF/web.xml.  This is used
 * for specifying the JSP configuration information on a JSP page
 *
 * @author Kin-man Chung
 */

public class JspConfig {

    private static final String WEB_XML = "/WEB-INF/web.xml";

    // Logger
    private static Logger log = Logger.getLogger(JspConfig.class.getName());

    private ArrayList<JspPropertyGroup> jspProperties = null;
    private ServletContext ctxt;
    private boolean initialized = false;

    private String defaultIsXml = null;		// unspecified
    private String defaultIsELIgnored = null;	// unspecified
    private String defaultIsScriptingInvalid = "false";
    private String defaultTrimSpaces = "false";
    private String defaultPoundAllowed = "false";
    private String defaultErrorOnUndeclaredNamespace = "false";
    private JspProperty defaultJspProperty;

    public JspConfig(ServletContext ctxt) {
	this.ctxt = ctxt;
    }

    private void processWebDotXml(ServletContext ctxt) throws JasperException {

        JspConfigDescriptor jspConfig = ctxt.getJspConfigDescriptor();
        if (jspConfig == null) {
            return;
        }

        jspProperties = new ArrayList<JspPropertyGroup>();
        for (JspPropertyGroupDescriptor jpg: jspConfig.getJspPropertyGroups()) {

            Collection<String> urlPatterns = jpg.getUrlPatterns();
            String pageEncoding = jpg.getPageEncoding();
            String scriptingInvalid = jpg.getScriptingInvalid();
            String elIgnored = jpg.getElIgnored();
            String isXml = jpg.getIsXml();
            String trimSpaces = jpg.getTrimDirectiveWhitespaces();
            String poundAllowed = jpg.getDeferredSyntaxAllowedAsLiteral();
            String buffer = jpg.getBuffer();
            String defaultContentType = jpg.getDefaultContentType();
            String errorOnUndeclaredNamespace = jpg.getErrorOnUndeclaredNamespace();
            ArrayList<String> includePrelude = new ArrayList<String>();
            includePrelude.addAll(jpg.getIncludePreludes());
            ArrayList<String> includeCoda = new ArrayList<String>();
            includeCoda.addAll(jpg.getIncludeCodas());

            // Creates a JspPropertyGroup for each url pattern in the given
            // urlPatterns, and adds it to the given jspProperties.
            // This simplifies the matching logic.

            for (String urlPattern: urlPatterns) {
                String path = null;
                String extension = null;

                if (urlPattern.indexOf('*') < 0) {
                    // Exact match
                    path = urlPattern;
                } else {
                    int i = urlPattern.lastIndexOf('/');
                    String file;
                    if (i >= 0) {
                        path = urlPattern.substring(0,i+1);
                        file = urlPattern.substring(i+1);
                    } else {
                        file = urlPattern;
                    }

                    // pattern must be "*", or of the form "*.jsp"
                    if (file.equals("*")) {
                        extension = "*";
                    } else if (file.startsWith("*.")) {
                        extension = file.substring(file.indexOf('.')+1);
                    }

                    // The url patterns are reconstructed as the following:
                    // path != null, extension == null:  / or /foo/bar.ext
                    // path == null, extension != null:  *.ext
                    // path != null, extension == "*":   /foo/*
                    boolean isStar = "*".equals(extension);
                    if ((path == null && (extension == null || isStar))
                            || (path != null && !isStar)) {
                        if (log.isLoggable(Level.WARNING)) {
                            log.warning(Localizer.getMessage(
                                "jsp.warning.bad.urlpattern.propertygroup",
                                urlPattern));
                        }
                        continue;
                    }
                 }

                 JspProperty property = new JspProperty(isXml,
                                                    elIgnored,
                                                    scriptingInvalid,
                                                    trimSpaces,
                                                    poundAllowed,
                                                    pageEncoding,
                                                    includePrelude,
                                                    includeCoda,
                                                    defaultContentType,
                                                    buffer,
                                                    errorOnUndeclaredNamespace);
                 JspPropertyGroup propertyGroup =
                     new JspPropertyGroup(path, extension, property);

                 jspProperties.add(propertyGroup);
            }
        }
    }

    private synchronized void init() throws JasperException {

	if (!initialized) {

            processWebDotXml(ctxt);
            if (ctxt.getEffectiveMajorVersion() < 2 ||
                    (ctxt.getEffectiveMajorVersion() == 2 &&
                     ctxt.getEffectiveMinorVersion() <= 3)) {
                // for version 2.3 or before, default for el-ignored is true
                defaultIsELIgnored = "true";
            }

	    defaultJspProperty = new JspProperty(defaultIsXml,
						 defaultIsELIgnored,
						 defaultIsScriptingInvalid,
                                                 defaultTrimSpaces,
                                                 defaultPoundAllowed,
                                                 null, null, null,
                                                 null, null,
                                                 defaultErrorOnUndeclaredNamespace);
	    initialized = true;
	}
    }

    /**
     * Select the property group that has more restrictive url-pattern.
     * In case of tie, select the first.
     */
    private JspPropertyGroup selectProperty(JspPropertyGroup prev,
                                            JspPropertyGroup curr) {
        if (prev == null) {
            return curr;
        }
        if (prev.getExtension() == null) {
            // exact match
            return prev;
        }
        if (curr.getExtension() == null) {
            // exact match
            return curr;
        }
        String prevPath = prev.getPath();
        String currPath = curr.getPath();
        if (prevPath == null && currPath == null) {
            // Both specifies a *.ext, keep the first one
            return prev;
        }
        if (prevPath == null && currPath != null) {
            return curr;
        }
        if (prevPath != null && currPath == null) {
            return prev;
        }
        if (prevPath.length() >= currPath.length()) {
            return prev;
        }
        return curr;
    }

    /**
     * Find a property that best matches the supplied resource.
     * @param uri the resource supplied.
     * @return a JspProperty indicating the best match, or some default.
     */
    public JspProperty findJspProperty(String uri) throws JasperException {

	init();

	// JSP Configuration settings do not apply to tag files
	if (jspProperties == null || uri.endsWith(".tag")
	        || uri.endsWith(".tagx")) {
	    return defaultJspProperty;
	}

	String uriPath = null;
	int index = uri.lastIndexOf('/');
	if (index >=0 ) {
	    uriPath = uri.substring(0, index+1);
	}
	String uriExtension = null;
	index = uri.lastIndexOf('.');
	if (index >=0) {
	    uriExtension = uri.substring(index+1);
	}

	ArrayList<String> includePreludes = new ArrayList<String>();
	ArrayList<String> includeCodas = new ArrayList<String>();

	JspPropertyGroup isXmlMatch = null;
	JspPropertyGroup elIgnoredMatch = null;
	JspPropertyGroup scriptingInvalidMatch = null;
	JspPropertyGroup trimSpacesMatch = null;
	JspPropertyGroup poundAllowedMatch = null;
	JspPropertyGroup pageEncodingMatch = null;
	JspPropertyGroup defaultContentTypeMatch = null;
	JspPropertyGroup bufferMatch = null;
	JspPropertyGroup errorOnUndeclaredNamespaceMatch = null;

        for (JspPropertyGroup jpg: jspProperties) {

	    JspProperty jp = jpg.getJspProperty();

             // (arrays will be the same length)
             String extension = jpg.getExtension();
             String path = jpg.getPath();

             if (extension == null) {
                 // exact match pattern: /a/foo.jsp
                 if (!uri.equals(path)) {
                     // not matched;
                     continue;
                 }
             } else {
                 // Matching patterns *.ext or /p/*
                 if (path != null && uriPath != null &&
                         ! uriPath.startsWith(path)) {
                     // not matched
                     continue;
                 }
                 if (!extension.equals("*") &&
                                 !extension.equals(uriExtension)) {
                     // not matched
                     continue;
                 }
             }
             // We have a match
             // Add include-preludes and include-codas
             if (jp.getIncludePrelude() != null) {
                 includePreludes.addAll(jp.getIncludePrelude());
             }
             if (jp.getIncludeCoda() != null) {
                 includeCodas.addAll(jp.getIncludeCoda());
             }

             // If there is a previous match for the same property, remember
             // the one that is more restrictive.
             if (jp.isXml() != null) {
                 isXmlMatch = selectProperty(isXmlMatch, jpg);
             }
             if (jp.isELIgnored() != null) {
                 elIgnoredMatch = selectProperty(elIgnoredMatch, jpg);
             }
             if (jp.isScriptingInvalid() != null) {
                 scriptingInvalidMatch =
                     selectProperty(scriptingInvalidMatch, jpg);
             }
             if (jp.getPageEncoding() != null) {
                 pageEncodingMatch = selectProperty(pageEncodingMatch, jpg);
             }
             if (jp.getTrimSpaces() != null) {
                 trimSpacesMatch = selectProperty(trimSpacesMatch, jpg);
             }
             if (jp.getPoundAllowed() != null) {
                 poundAllowedMatch = selectProperty(poundAllowedMatch, jpg);
             }
             if (jp.getDefaultContentType() != null) {
                 defaultContentTypeMatch =
                     selectProperty(defaultContentTypeMatch, jpg);
             }
             if (jp.getBuffer() != null) {
                 bufferMatch = selectProperty(bufferMatch, jpg);
             }
             if (jp.errorOnUndeclaredNamespace() != null) {
                 errorOnUndeclaredNamespaceMatch =
                     selectProperty(errorOnUndeclaredNamespaceMatch, jpg);
             }
	}

	String isXml = defaultIsXml;
	String isELIgnored = defaultIsELIgnored;
	String isScriptingInvalid = defaultIsScriptingInvalid;
        String trimSpaces = defaultTrimSpaces;
        String poundAllowed = defaultPoundAllowed;
	String pageEncoding = null;
        String defaultContentType = null;
        String buffer = null;
        String errorOnUndeclaredNamespace = defaultErrorOnUndeclaredNamespace;

	if (isXmlMatch != null) {
	    isXml = isXmlMatch.getJspProperty().isXml();
	}
	if (elIgnoredMatch != null) {
	    isELIgnored = elIgnoredMatch.getJspProperty().isELIgnored();
	}
	if (scriptingInvalidMatch != null) {
	    isScriptingInvalid =
		scriptingInvalidMatch.getJspProperty().isScriptingInvalid();
	}
	if (trimSpacesMatch != null) {
	    trimSpaces = trimSpacesMatch.getJspProperty().getTrimSpaces();
	}
	if (poundAllowedMatch != null) {
	    poundAllowed = poundAllowedMatch.getJspProperty().getPoundAllowed();
	}
	if (pageEncodingMatch != null) {
	    pageEncoding = pageEncodingMatch.getJspProperty().getPageEncoding();
	}
	if (defaultContentTypeMatch != null) {
	    defaultContentType =
                defaultContentTypeMatch.getJspProperty().getDefaultContentType();
	}
	if (bufferMatch != null) {
	    buffer = bufferMatch.getJspProperty().getBuffer();
	}
	if (errorOnUndeclaredNamespaceMatch != null) {
	    errorOnUndeclaredNamespace = errorOnUndeclaredNamespaceMatch.
                getJspProperty().errorOnUndeclaredNamespace();
	}

	return new JspProperty(isXml, isELIgnored, isScriptingInvalid,
                               trimSpaces, poundAllowed,
			       pageEncoding, includePreludes, includeCodas,
                               defaultContentType, buffer,
                               errorOnUndeclaredNamespace);
    }

    /**
     * To find out if an uri matches an url pattern in jsp config.  If so,
     * then the uri is a JSP page.  This is used primarily for jspc.
     */
    public boolean isJspPage(String uri) throws JasperException {

        init();
        if (jspProperties == null) {
            return false;
        }

        String uriPath = null;
        int index = uri.lastIndexOf('/');
        if (index >=0 ) {
            uriPath = uri.substring(0, index+1);
        }
        String uriExtension = null;
        index = uri.lastIndexOf('.');
        if (index >=0) {
            uriExtension = uri.substring(index+1);
        }

        for (JspPropertyGroup jpg: jspProperties) {

            JspProperty jp = jpg.getJspProperty();

            String extension = jpg.getExtension();
            String path = jpg.getPath();

            if (extension == null) {
                if (uri.equals(path)) {
                    // There is an exact match
                    return true;
                }
            } else {
                if ((path == null || path.equals(uriPath)) &&
                    (extension.equals("*") || extension.equals(uriExtension))) {
                    // Matches *, *.ext, /p/*, or /p/*.ext
                    return true;
                }
            }
        }
        return false;
    }
}


/*
 * 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.IOException;
import java.io.PrintWriter;

/**
 * This is what is used to generate servlets.
 *
 * @author Anil K. Vijendran
 * @author Kin-man Chung
 */
public class ServletWriter {
    public static final int TAB_WIDTH = 2;
    public static final String SPACES = "                              ";

    // Current indent level:
    private int indent = 0;
    private int virtual_indent = 0;

    // The sink writer:
    PrintWriter writer;

    // servlet line numbers start from 1
    private int javaLine = 1;

    public ServletWriter(PrintWriter writer) {
	this.writer = writer;
    }

    public void close() throws IOException {
	writer.close();
    }

    // -------------------- Access informations --------------------

    public int getJavaLine() {
        return javaLine;
    }

    // -------------------- Formatting --------------------

    public void pushIndent() {
	virtual_indent += TAB_WIDTH;
	if (virtual_indent >= 0 && virtual_indent <= SPACES.length())
	    indent = virtual_indent;
    }

    public void popIndent() {
	virtual_indent -= TAB_WIDTH;
	if (virtual_indent >= 0 && virtual_indent <= SPACES.length())
	    indent = virtual_indent;
    }

    /**
     * Print a standard comment for echo outputed chunk.
     * @param start The starting position of the JSP chunk being processed.
     * @param stop  The ending position of the JSP chunk being processed.
     */
    public void printComment(Mark start, Mark stop, char[] chars) {
        if (start != null && stop != null) {
            println("// from="+start);
            println("//   to="+stop);
        }

        if (chars != null)
            for(int i = 0; i < chars.length;) {
                printin();
                print("// ");
                while (chars[i] != '\n' && i < chars.length)
                    writer.print(chars[i++]);
            }
    }

    /**
     * Prints the given string followed by '\n'
     */
    public void println(String s) {
        javaLine++;
	writer.println(s);
    }

    /**
     * Prints a '\n'
     */
    public void println() {
        javaLine++;
	writer.println("");
    }

    /**
     * Prints the current indention
     */
    public void printin() {
	writer.print(SPACES.substring(0, indent));
    }

    /**
     * Prints the current indention, followed by the given string
     */
    public void printin(String s) {
	writer.print(SPACES.substring(0, indent));
	writer.print(s);
    }

    /**
     * Prints the current indention, and then the string, and a '\n'.
     */
    public void printil(String s) {
        javaLine++;
	writer.print(SPACES.substring(0, indent));
	writer.println(s);
    }

    /**
     * Prints the given char.
     *
     * Use println() to print a '\n'.
     */
    public void print(char c) {
	writer.print(c);
    }

    /**
     * Prints the given int.
     */
    public void print(int i) {
	writer.print(i);
    }

    /**
     * Prints the given string.
     *
     * The string must not contain any '\n', otherwise the line count will be
     * off.
     */
    public void print(String s) {
	writer.print(s);
    }

    /**
     * Prints the given string.
     *
     * If the string spans multiple lines, the line count will be adjusted
     * accordingly.
     */
    public void printMultiLn(String s) {
        int index = 0;

        // look for hidden newlines inside strings
        while ((index=s.indexOf('\n',index)) > -1 ) {
            javaLine++;
            index++;
        }

	writer.print(s);
    }
}


/*
 * 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.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.el.ELException;
import javax.el.FunctionMapper;
import javax.el.ExpressionFactory;

import org.apache.jasper.Constants;
import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;
import org.apache.jasper.runtime.ELContextImpl;
import org.xml.sax.Attributes;

/**
 * This class has all the utility method(s).
 * Ideally should move all the bean containers here.
 *
 * @author Mandar Raje.
 * @author Rajiv Mordani.
 * @author Danno Ferrin
 * @author Pierre Delisle
 * @author Shawn Bayern
 * @author Mark Roth
 * @author Kin-man Chung
 */
public class JspUtil {

    private static final String WEB_INF_TAGS = "/WEB-INF/tags/";
    private static final String META_INF_TAGS = "/META-INF/tags/";

    // Delimiters for request-time expressions (JSP and XML syntax)
    private static final String OPEN_EXPR  = "<%=";
    private static final String CLOSE_EXPR = "%>";
    private static final String OPEN_EXPR_XML  = "%=";
    private static final String CLOSE_EXPR_XML = "%";

    private static int tempSequenceNumber = 0;
    private static ExpressionFactory expFactory;

    private static final String javaKeywords[] = {
        "abstract", "assert", "boolean", "break", "byte", "case",
        "catch", "char", "class", "const", "continue",
        "default", "do", "double", "else", "enum", "extends",
        "final", "finally", "float", "for", "goto",
        "if", "implements", "import", "instanceof", "int",
        "interface", "long", "native", "new", "package",
        "private", "protected", "public", "return", "short",
        "static", "strictfp", "super", "switch", "synchronized",
        "this", "throws", "transient", "try", "void",
        "volatile", "while" };

    public static final int CHUNKSIZE = 1024;

    public static char[] removeQuotes(char []chars) {
        CharArrayWriter caw = new CharArrayWriter();
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == '%' && chars[i+1] == '\\' &&
                chars[i+2] == '>') {
                caw.write('%');
                caw.write('>');
                i = i + 2;
            } else {
                caw.write(chars[i]);
            }
        }
        return caw.toCharArray();
    }

    public static char[] escapeQuotes (char []chars) {
        // Prescan to convert %\> to %>
        String s = new String(chars);
        while (true) {
            int n = s.indexOf("%\\>");
            if (n < 0)
                break;
            StringBuilder sb = new StringBuilder(s.substring(0, n));
            sb.append("%>");
            sb.append(s.substring(n + 3));
            s = sb.toString();
        }
        chars = s.toCharArray();
        return (chars);

        // Escape all backslashes not inside a Java string literal
        /*
        CharArrayWriter caw = new CharArrayWriter();
        boolean inJavaString = false;
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == '"') inJavaString = !inJavaString;
            // escape out the escape character
            if (!inJavaString && (chars[i] == '\\')) caw.write('\\');
            caw.write(chars[i]);
        }
        return caw.toCharArray();
        */
    }

    /**
     * Checks if the token is a runtime expression.
     * In standard JSP syntax, a runtime expression starts with '<%' and
     * ends with '%>'. When the JSP document is in XML syntax, a runtime
     * expression starts with '%=' and ends with '%'.
     *
     * @param token The token to be checked
     * return whether the token is a runtime expression or not.
     */
    public static boolean isExpression(String token, boolean isXml) {
	String openExpr;
	String closeExpr;
	if (isXml) {
	    openExpr = OPEN_EXPR_XML;
	    closeExpr = CLOSE_EXPR_XML;
	} else {
	    openExpr = OPEN_EXPR;
	    closeExpr = CLOSE_EXPR;
	}
	if (token.startsWith(openExpr) && token.endsWith(closeExpr)) {
	    return true;
	} else {
	    return false;
	}
    }

    /**
     * @return the "expression" part of a runtime expression,
     * taking the delimiters out.
     */
    public static String getExpr (String expression, boolean isXml) {
	String returnString;
	String openExpr;
	String closeExpr;
	if (isXml) {
	    openExpr = OPEN_EXPR_XML;
	    closeExpr = CLOSE_EXPR_XML;
	} else {
	    openExpr = OPEN_EXPR;
	    closeExpr = CLOSE_EXPR;
	}
	int length = expression.length();
	if (expression.startsWith(openExpr) &&
                expression.endsWith(closeExpr)) {
	    returnString = expression.substring(
                               openExpr.length(), length - closeExpr.length());
	} else {
	    returnString = "";
	}
	return returnString;
    }

    /**
     * Takes a potential expression and converts it into XML form
     */
    public static String getExprInXml(String expression) {
        String returnString;
        int length = expression.length();

        if (expression.startsWith(OPEN_EXPR)
                && expression.endsWith(CLOSE_EXPR)) {
            returnString = expression.substring (1, length - 1);
        } else {
            returnString = expression;
        }

        return escapeXml(returnString);
    }

    /**
     * Checks to see if the given scope is valid.
     *
     * @param scope The scope to be checked
     * @param n The Node containing the 'scope' attribute whose value is to be
     * checked
     * @param err error dispatcher
     *
     * @throws JasperException if scope is not null and different from
     * "page", "request", "session", and
     * "application"
     */
    public static void checkScope(String scope, Node n, ErrorDispatcher err)
            throws JasperException {
	if (scope != null && !scope.equals("page") && !scope.equals("request")
		&& !scope.equals("session") && !scope.equals("application")) {
	    err.jspError(n, "jsp.error.invalid.scope", scope);
	}
    }

    /**
     * Checks if all mandatory attributes are present and if all attributes
     * present have valid names.  Checks attributes specified as XML-style
     * attributes as well as attributes specified using the jsp:attribute
     * standard action.
     */
    public static void checkAttributes(String typeOfTag,
				       Node n,
				       ValidAttribute[] validAttributes,
				       ErrorDispatcher err)
				throws JasperException {
        Attributes attrs = n.getAttributes();
        Mark start = n.getStart();
	boolean valid = true;

        // AttributesImpl.removeAttribute is broken, so we do this...
        int tempLength = (attrs == null) ? 0 : attrs.getLength();
	ArrayList<String> temp = new ArrayList<String>(tempLength);
        for (int i = 0; i < tempLength; i++) {
            String qName = attrs.getQName(i);
            if ((!qName.equals("xmlns")) && (!qName.startsWith("xmlns:")))
                temp.add(qName);
        }

        // Add names of attributes specified using jsp:attribute
        Node.Nodes tagBody = n.getBody();
        if( tagBody != null ) {
            int numSubElements = tagBody.size();
            for( int i = 0; i < numSubElements; i++ ) {
                Node node = tagBody.getNode( i );
                if( node instanceof Node.NamedAttribute ) {
                    String attrName = node.getAttributeValue( "name" );
                    temp.add( attrName );
		    // Check if this value appear in the attribute of the node
		    if (n.getAttributeValue(attrName) != null) {
			err.jspError(n, "jsp.error.duplicate.name.jspattribute",
					attrName);
		    }
                }
                else {
                    // Nothing can come before jsp:attribute, and only
                    // jsp:body can come after it.
                    break;
                }
            }
        }

	/*
	 * First check to see if all the mandatory attributes are present.
	 * If so only then proceed to see if the other attributes are valid
	 * for the particular tag.
	 */
	String missingAttribute = null;

	for (int i = 0; i < validAttributes.length; i++) {
	    int attrPos;
	    if (validAttributes[i].mandatory) {
                attrPos = temp.indexOf(validAttributes[i].name);
		if (attrPos != -1) {
		    temp.remove(attrPos);
		    valid = true;
		} else {
		    valid = false;
		    missingAttribute = validAttributes[i].name;
		    break;
		}
	    }
	}

	// If mandatory attribute is missing then the exception is thrown
	if (!valid)
	    err.jspError(start, "jsp.error.mandatory.attribute", typeOfTag,
			 missingAttribute);

	// Check to see if there are any more attributes for the specified tag.
        int attrLeftLength = temp.size();
	if (attrLeftLength == 0)
	    return;

	// Now check to see if the rest of the attributes are valid too.
	String attribute = null;

	for (int j = 0; j < attrLeftLength; j++) {
	    valid = false;
	    attribute = temp.get(j);
	    for (int i = 0; i < validAttributes.length; i++) {
		if (attribute.equals(validAttributes[i].name)) {
		    valid = true;
		    break;
		}
	    }
	    if (!valid)
		err.jspError(start, "jsp.error.invalid.attribute", typeOfTag,
			     attribute);
	}
	// XXX *could* move EL-syntax validation here... (sb)
    }

    public static String escapeQueryString(String unescString) {
	if ( unescString == null )
	    return null;

	String escString    = "";
	String shellSpChars = "\\\"";

	for(int index=0; index<unescString.length(); index++) {
	    char nextChar = unescString.charAt(index);

	    if( shellSpChars.indexOf(nextChar) != -1 )
		escString += "\\";

	    escString += nextChar;
	}
	return escString;
    }

    /**
     *  Escape the 5 entities defined by XML.
     */
    public static String escapeXml(String s) {
        if (s == null) return null;
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<s.length(); i++) {
            char c = s.charAt(i);
            if (c == '<') {
                sb.append("<");
            } else if (c == '>') {
                sb.append(">");
            } else if (c == '\'') {
                sb.append("'");
            } else if (c == '&') {
                sb.append("&");
            } else if (c == '"') {
                sb.append(""");
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * Replaces any occurrences of the character <tt>replace</tt> with the
     * string <tt>with</tt>.
     */
    public static String replace(String name, char replace, String with) {
	StringBuilder buf = new StringBuilder();
	int begin = 0;
	int end;
	int last = name.length();

	while (true) {
	    end = name.indexOf(replace, begin);
	    if (end < 0) {
		end = last;
	    }
	    buf.append(name.substring(begin, end));
	    if (end == last) {
		break;
	    }
	    buf.append(with);
	    begin = end + 1;
	}

	return buf.toString();
    }

    public static class ValidAttribute {
	String name;
	boolean mandatory;

	public ValidAttribute (String name, boolean mandatory)
        {
	    this.name = name;
	    this.mandatory = mandatory;
        }

	public ValidAttribute (String name) {
	    this (name, false);
	}
    }

    /**
     * Convert a String value to 'boolean'.
     * Besides the standard conversions done by
     * Boolean.valueOf(s).booleanValue(), the value "yes"
     * (ignore case) is also converted to 'true'.
     * If 's' is null, then 'false' is returned.
     *
     * @param s the string to be converted
     * @return the boolean value associated with the string s
     */
    public static boolean booleanValue(String s) {
	boolean b = false;
	if (s != null) {
	    if (s.equalsIgnoreCase("yes")) {
		b = true;
	    } else {
		b = Boolean.valueOf(s).booleanValue();
	    }
	}
	return b;
    }

    /**
     * Returns the <tt>Class</tt> object associated with the class or
     * interface with the given string name.
     *
     * <p> The <tt>Class</tt> object is determined by passing the given string
     * name to the <tt>Class.forName()</tt> method, unless the given string
     * name represents a primitive type, in which case it is converted to a
     * <tt>Class</tt> object by appending ".class" to it (e.g., "int.class").
     */
    public static Class<?> toClass(String type, ClassLoader loader)
	    throws ClassNotFoundException {

	Class<?> c = null;
	int i0 = type.indexOf('[');
	int dims = 0;
	if (i0 > 0) {
	    // This is an array.  Count the dimensions
	    for (int i = 0; i < type.length(); i++) {
		if (type.charAt(i) == '[')
		    dims++;
	    }
	    type = type.substring(0, i0);
	}

	if ("boolean".equals(type))
	    c = boolean.class;
	else if ("char".equals(type))
	    c = char.class;
	else if ("byte".equals(type))
	    c =  byte.class;
	else if ("short".equals(type))
	    c = short.class;
	else if ("int".equals(type))
	    c = int.class;
	else if ("long".equals(type))
	    c = long.class;
	else if ("float".equals(type))
	    c = float.class;
	else if ("double".equals(type))
	    c = double.class;
	else if (type.indexOf('[') < 0)
	    c = loader.loadClass(type);

	if (dims == 0)
	    return c;

	if (dims == 1)
	    return java.lang.reflect.Array.newInstance(c, 1).getClass();

	// Array of more than i dimension
	return java.lang.reflect.Array.newInstance(c, new int[dims]).getClass();
    }

    /**
     * Produces a String representing a call to the EL interpreter.
     * @param expression a String containing zero or more "${}" expressions
     * @param expectedType the expected type of the interpreted result
     * @param fnmapvar Variable pointing to a function map.
     * @return a String representing a call to the EL interpreter.
     */
    public static String interpreterCall(boolean isTagFile,
					 String expression,
                                         Class expectedType,
                                         String fnmapvar,
                                         String expectedDeferredType,
                                         String expectedReturnType,
                                         String[] expectedParamTypes
                                         )
    {
        /*
         * Determine which context object to use.
         */
	String jspCtxt = null;
	if (isTagFile)
	    jspCtxt = "this.getJspContext()";
	else
	    jspCtxt = "_jspx_page_context";

        if (expectedType == javax.el.ValueExpression.class) {

            if (expectedDeferredType == null) {
                expectedDeferredType = "java.lang.Object";
            }

            return "org.apache.jasper.runtime.PageContextImpl.getValueExpression"
               + "(" + Generator.quote(expression) + ", "
               + "(PageContext)" + jspCtxt + ", "
               + expectedDeferredType + ".class, "
               + fnmapvar + ")";

        }

        if (expectedType == javax.el.MethodExpression.class) {

            if (expectedReturnType == null) {
                expectedReturnType = "Void";
            }

            StringBuilder params = new StringBuilder();
            if (expectedParamTypes != null) {
                for (int i = 0; i < expectedParamTypes.length; i++) {
                    if (i > 0) {
                        params.append(", ");
                    }
                    params.append(expectedParamTypes[i] + ".class");
                }
            }

            return "org.apache.jasper.runtime.PageContextImpl.getMethodExpression"
               + "(" + Generator.quote(expression) + ", "
               + "(PageContext)" + jspCtxt + ", "
               + fnmapvar + ", "
               + expectedReturnType + ".class, "
               + "new Class[] {" + params.toString() + "})";
        }

	/*
         * Determine whether to use the expected type's textual name
	 * or, if it's a primitive, the name of its correspondent boxed
	 * type.
         */
	String targetType = expectedType.getName();
	String primitiveConverterMethod = null;
	if (expectedType.isPrimitive()) {
	    if (expectedType.equals(Boolean.TYPE)) {
		targetType = Boolean.class.getName();
		primitiveConverterMethod = "booleanValue";
	    } else if (expectedType.equals(Byte.TYPE)) {
		targetType = Byte.class.getName();
		primitiveConverterMethod = "byteValue";
	    } else if (expectedType.equals(Character.TYPE)) {
		targetType = Character.class.getName();
		primitiveConverterMethod = "charValue";
	    } else if (expectedType.equals(Short.TYPE)) {
		targetType = Short.class.getName();
		primitiveConverterMethod = "shortValue";
	    } else if (expectedType.equals(Integer.TYPE)) {
		targetType = Integer.class.getName();
		primitiveConverterMethod = "intValue";
	    } else if (expectedType.equals(Long.TYPE)) {
		targetType = Long.class.getName();
		primitiveConverterMethod = "longValue";
	    } else if (expectedType.equals(Float.TYPE)) {
		targetType = Float.class.getName();
		primitiveConverterMethod = "floatValue";
	    } else if (expectedType.equals(Double.TYPE)) {
		targetType = Double.class.getName();
		primitiveConverterMethod = "doubleValue";
	    }
	}

	targetType = toJavaSourceType(targetType);

	StringBuilder call = new StringBuilder(
             "(" + targetType + ") "
               + "org.apache.jasper.runtime.PageContextImpl.evaluateExpression"
               + "(" + Generator.quote(expression) + ", "
               +       targetType + ".class, "
	       +       "(PageContext)" + jspCtxt
               +       ", " + fnmapvar
               + ")");

	/*
         * Add the primitive converter method if we need to.
         */
	if (primitiveConverterMethod != null) {
	    call.insert(0, "(");
	    call.append(")." + primitiveConverterMethod + "()");
	}

	return call.toString();
    }

    /**
     * Validates the syntax of all EL expressions within the given string.
     * @param where the approximate location of the expressions in the JSP page
     * @param expressions a string containing an EL expressions
     * @param err an error dispatcher to use
     */
    public static void validateExpressions(Mark where,
                                           String expressions,
                                           FunctionMapper functionMapper,
                                           ErrorDispatcher err)
            throws JasperException {

        try {
            ELContextImpl elContext = new ELContextImpl(null);
            elContext.setFunctionMapper(functionMapper);
            getExpressionFactory().createValueExpression(
                elContext,
                expressions, Object.class);
        }
        catch( ELException e ) {
            err.jspError(where, "jsp.error.invalid.expression", expressions,
                e.toString() );
        }
    }

    public static Object coerce(Class targetType, String value)
            throws ELException {
        return getExpressionFactory().coerceToType(value, targetType);
    }

    /**
     * Resets the temporary variable name.
     * (not thread-safe)
     */
    public static void resetTemporaryVariableName() {
        tempSequenceNumber = 0;
    }

    /**
     * Generates a new temporary variable name.
     * (not thread-safe)
     */
    public static String nextTemporaryVariableName() {
        return Constants.TEMP_VARIABLE_NAME_PREFIX + (tempSequenceNumber++);
    }

    public static String coerceToPrimitiveBoolean(String s,
						  boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToBoolean(" + s + ")";
	} else {
	    if (s == null || s.length() == 0)
		return "false";
	    else
		return Boolean.valueOf(s).toString();
	}
    }

    public static String coerceToBoolean(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Boolean) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Boolean.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "Boolean.FALSE";
	    } else {
		// Detect format error at translation time
		return "new Boolean(" + Boolean.valueOf(s).toString() + ")";
	    }
	}
    }

    public static String coerceToPrimitiveByte(String s,
					       boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToByte(" + s + ")";
	} else {
	    if (s == null || s.length() == 0)
		return "(byte) 0";
	    else
		return "((byte)" + Byte.valueOf(s).toString() + ")";
	}
    }

    public static String coerceToByte(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Byte) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Byte.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "Byte.valueOf((byte) 0)";
	    } else {
		// Detect format error at translation time
		return "new Byte((byte)" + Byte.valueOf(s).toString() + ")";
	    }
	}
    }

    public static String coerceToChar(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToChar(" + s + ")";
	} else {
	    if (s == null || s.length() == 0) {
		return "(char) 0";
	    } else {
		char ch = s.charAt(0);
		// this trick avoids escaping issues
		return "((char) " + (int) ch + ")";
	    }
	}
    }

    public static String coerceToCharacter(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Character) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Character.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "new Character((char) 0)";
	    } else {
		char ch = s.charAt(0);
		// this trick avoids escaping issues
		return "new Character((char) " + (int) ch + ")";
	    }
	}
    }

    public static String coerceToPrimitiveDouble(String s,
						 boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToDouble(" + s + ")";
	} else {
	    if (s == null || s.length() == 0)
		return "(double) 0";
	    else
		return Double.valueOf(s).toString();
	}
    }

    public static String coerceToDouble(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Double) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Double.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "new Double(0)";
	    } else {
		// Detect format error at translation time
		return "new Double(" + Double.valueOf(s).toString() + ")";
	    }
	}
    }

    public static String coerceToPrimitiveFloat(String s,
						boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToFloat(" + s + ")";
	} else {
	    if (s == null || s.length() == 0)
		return "(float) 0";
	    else
		return Float.valueOf(s).toString() + "f";
	}
    }

    public static String coerceToFloat(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Float) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Float.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "Float.valueOf(0)";
	    } else {
		// Detect format error at translation time
		return "new Float(" + Float.valueOf(s).toString() + "f)";
	    }
	}
    }

    public static String coerceToInt(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToInt(" + s + ")";
	} else {
	    if (s == null || s.length() == 0)
		return "0";
	    else
		return Integer.valueOf(s).toString();
	}
    }

    public static String coerceToInteger(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Integer) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Integer.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "Integer.valueOf(0)";
	    } else {
		// Detect format error at translation time
		return "new Integer(" + Integer.valueOf(s).toString() + ")";
	    }
	}
    }

    public static String coerceToPrimitiveShort(String s,
						boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToShort(" + s + ")";
	} else {
	    if (s == null || s.length() == 0)
		return "(short) 0";
	    else
		return "((short) " + Short.valueOf(s).toString() + ")";
	}
    }

    public static String coerceToShort(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Short) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Short.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "Short.valueOf((short) 0)";
	    } else {
		// Detect format error at translation time
		return "new Short(\"" + Short.valueOf(s).toString() + "\")";
	    }
	}
    }

    public static String coerceToPrimitiveLong(String s,
					       boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToLong(" + s + ")";
	} else {
	    if (s == null || s.length() == 0)
		return "(long) 0";
	    else
		return Long.valueOf(s).toString() + "l";
	}
    }

    public static String coerceToLong(String s, boolean isNamedAttribute) {
	if (isNamedAttribute) {
	    return "(Long) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + s + ", Long.class)";
	} else {
	    if (s == null || s.length() == 0) {
		return "Long.valueOf(0)";
	    } else {
		// Detect format error at translation time
		return "new Long(" + Long.valueOf(s).toString() + "l)";
	    }
	}
    }

    public static String coerceToEnum(String s, String enumClass, boolean isNamedAttribute) {
        if (isNamedAttribute) {
            return "org.apache.jasper.runtime.JspRuntimeLibrary.coerce("
                    + s + "," + enumClass + ".class)";
        } else {
            if (s == null || s.length() == 0) {
                return "null";
            } else {
                return "Enum.valueOf(" + enumClass + ".class, \"" + s + "\")";
            }
        }
    }

    public static InputStream getInputStream(String fname, JarFile jarFile,
					     JspCompilationContext ctxt,
					     ErrorDispatcher err)
		throws JasperException, IOException {

        InputStream in = null;

	if (jarFile != null) {
	    String jarEntryName = fname.substring(1, fname.length());
	    ZipEntry jarEntry = jarFile.getEntry(jarEntryName);
	    if (jarEntry == null) {
		err.jspError("jsp.error.file.not.found", fname);
	    }
	    in = jarFile.getInputStream(jarEntry);
	} else {
	    in = ctxt.getResourceAsStream(fname);
	}

	if (in == null) {
	    err.jspError("jsp.error.file.not.found", fname);
	}

	return in;
    }

    /**
     * Gets the fully-qualified class name of the tag handler corresponding to
     * the given tag file path.
     *
     * @param path Tag file path
     * @param err Error dispatcher
     *
     * @return Fully-qualified class name of the tag handler corresponding to
     * the given tag file path
     */
    public static String getTagHandlerClassName(String path,
						ErrorDispatcher err)
                throws JasperException {

        String className = null;
        int begin = 0;
        int index;

        index = path.lastIndexOf(".tag");
        if (index == -1) {
            err.jspError("jsp.error.tagfile.badSuffix", path);
        }

        //It's tempting to remove the ".tag" suffix here, but we can't.
        //If we remove it, the fully-qualified class name of this tag
        //could conflict with the package name of other tags.
        //For instance, the tag file
        //    /WEB-INF/tags/foo.tag
        //would have fully-qualified class name
        //    org.apache.jsp.tag.web.foo
        //which would conflict with the package name of the tag file
        //    /WEB-INF/tags/foo/bar.tag

        index = path.indexOf(WEB_INF_TAGS);
        if (index != -1) {
            className = "org.apache.jsp.tag.web.";
            begin = index + WEB_INF_TAGS.length();
        } else {
	    index = path.indexOf(META_INF_TAGS);
	    if (index != -1) {
		className = "org.apache.jsp.tag.meta.";
		begin = index + META_INF_TAGS.length();
	    } else {
		err.jspError("jsp.error.tagfile.illegalPath", path);
	    }
	}

        className += makeJavaPackage(path.substring(begin));

       return className;
    }

    /**
     * Converts the given path to a Java package or fully-qualified class name
     *
     * @param path Path to convert
     *
     * @return Java package corresponding to the given path
     */
    public static final String makeJavaPackage(String path) {
        String classNameComponents[] = split(path,"/");
        StringBuilder legalClassNames = new StringBuilder();
        for (int i = 0; i < classNameComponents.length; i++) {
            legalClassNames.append(makeJavaIdentifier(classNameComponents[i]));
            if (i < classNameComponents.length - 1) {
                legalClassNames.append('.');
            }
        }
        return legalClassNames.toString();
    }

    /**
     * Splits a string into it's components.
     * @param path String to split
     * @param pat Pattern to split at
     * @return the components of the path
     */
    private static final String [] split(String path, String pat) {
        ArrayList<String> comps = new ArrayList<String>();
        int pos = path.indexOf(pat);
        int start = 0;
        while( pos >= 0 ) {
            if(pos > start ) {
                String comp = path.substring(start,pos);
                comps.add(comp);
            }
            start = pos + pat.length();
            pos = path.indexOf(pat,start);
        }
        if( start < path.length()) {
            comps.add(path.substring(start));
        }
        String [] result = new String[comps.size()];
        for(int i=0; i < comps.size(); i++) {
            result[i] = comps.get(i);
        }
        return result;
    }

    /**
     * Converts the given identifier to a legal Java identifier
     *
     * @param identifier Identifier to convert
     *
     * @return Legal Java identifier corresponding to the given identifier
     */
    public static final String makeJavaIdentifier(String identifier) {
        StringBuilder modifiedIdentifier =
            new StringBuilder(identifier.length());
        if (!Character.isJavaIdentifierStart(identifier.charAt(0))) {
            modifiedIdentifier.append('_');
        }
        for (int i = 0; i < identifier.length(); i++) {
            char ch = identifier.charAt(i);
            if (Character.isJavaIdentifierPart(ch) && ch != '_') {
                modifiedIdentifier.append(ch);
            } else if (ch == '.') {
                modifiedIdentifier.append('_');
            } else {
                modifiedIdentifier.append(mangleChar(ch));
            }
        }
        if (isJavaKeyword(modifiedIdentifier.toString())) {
            modifiedIdentifier.append('_');
        }
        return modifiedIdentifier.toString();
    }

    /**
     * Mangle the specified character to create a legal Java class name.
     */
    public static final String mangleChar(char ch) {
        char[] result = new char[5];
        result[0] = '_';
        result[1] = Character.forDigit((ch >> 12) & 0xf, 16);
        result[2] = Character.forDigit((ch >> 8) & 0xf, 16);
        result[3] = Character.forDigit((ch >> 4) & 0xf, 16);
        result[4] = Character.forDigit(ch & 0xf, 16);
        return new String(result);
    }

    /**
     * Test whether the argument is a Java keyword
     */
    public static boolean isJavaKeyword(String key) {
        int i = 0;
        int j = javaKeywords.length;
        while (i < j) {
            int k = (i+j)/2;
            int result = javaKeywords[k].compareTo(key);
            if (result == 0) {
                return true;
            }
            if (result < 0) {
                i = k+1;
            } else {
                j = k;
            }
        }
        return false;
    }

    /**
     * Converts the given Xml name to a legal Java identifier.  This is
     * slightly more efficient than makeJavaIdentifier in that we only need
     * to worry about '.', '-', and ':' in the string.  We also assume that
     * the resultant string is further concatenated with some prefix string
     * so that we don't have to worry about it being a Java key word.
     *
     * @param name Identifier to convert
     *
     * @return Legal Java identifier corresponding to the given identifier
     */
    public static final String makeXmlJavaIdentifier(String name) {
        if (name.indexOf('-') >= 0)
            name = replace(name, '-', "$1");
        if (name.indexOf('.') >= 0)
            name = replace(name, '.', "$2");
        if (name.indexOf(':') >= 0)
            name = replace(name, ':', "$3");
        return name;
    }

    static InputStreamReader getReader(String fname, String encoding,
				       JarFile jarFile,
				       JspCompilationContext ctxt,
				       ErrorDispatcher err)
		throws JasperException, IOException {

        InputStreamReader reader = null;
	InputStream in = getInputStream(fname, jarFile, ctxt, err);

	try {
            reader = new InputStreamReader(in, encoding);
	} catch (UnsupportedEncodingException ex) {
	    err.jspError("jsp.error.unsupported.encoding", encoding);
	}

	return reader;
    }

    /**
     * Class.getName() return arrays in the form "[[[<et>", where et,
     * the element type can be one of ZBCDFIJS or L<classname>;
     * It is converted into forms that can be understood by javac.
     */
    public static String toJavaSourceType(String type) {

	if (type.charAt(0) != '[') {
	    return type;
 	}

	int dims = 1;
	String t = null;
	for (int i = 1; i < type.length(); i++) {
	    if (type.charAt(i) == '[') {
		dims++;
	    } else {
		switch (type.charAt(i)) {
		case 'Z': t = "boolean"; break;
		case 'B': t = "byte"; break;
		case 'C': t = "char"; break;
		case 'D': t = "double"; break;
		case 'F': t = "float"; break;
		case 'I': t = "int"; break;
		case 'J': t = "long"; break;
		case 'S': t = "short"; break;
		case 'L': t = type.substring(i+1, type.indexOf(';')); break;
		}
		break;
	    }
	}
	StringBuilder resultType = new StringBuilder(t);
	for (; dims > 0; dims--) {
	    resultType.append("[]");
	}
	return resultType.toString();
    }

    /**
     * Compute the canonical name from a Class instance.  Note that a
     * simple replacment of '$' with '.' of a binary name would not work,
     * as '$' is a legal Java Identifier character.
     * @param c A instance of java.lang.Class
     * @return  The canonical name of c.
     */
    public static String getCanonicalName(Class c) {

        String binaryName = c.getName();
        c = c.getDeclaringClass();

        if (c == null) {
            return binaryName;
        }

        StringBuilder buf = new StringBuilder(binaryName);
        do {
            buf.setCharAt(c.getName().length(), '.');
            c = c.getDeclaringClass();
        } while ( c != null);

        return buf.toString();
    }

    private static ExpressionFactory getExpressionFactory() {
        if (expFactory == null) {
            expFactory = ExpressionFactory.newInstance();
        }
        return expFactory;
    }

    static Map<String, Manifest> manifestMap =
        new ConcurrentHashMap<String, Manifest>();
    static Manifest nullManifest = new Manifest();

    /**
     * Given a list of jar files, their manifest attribute Class-path are
     * scanned, and jars specified there are added to the list.  This is
     * carried out recursively.
     * Note:  This is needed to work around the JDK bug 6725230.
     */
    public static List<String> expandClassPath(List<String>files) {

        for (int i = 0; i < files.size(); i++) {
            String file = files.get(i);
            if (! file.endsWith(".jar")) {
                continue;
            }
            Manifest manifest = manifestMap.get(file);
            JarFile jarfile = null;
            if (manifest == null) {
                try {
                    jarfile = new JarFile(file, false);
                    manifest = jarfile.getManifest();
                    if (manifest == null) {
                        // mark jar file as known to contain no manifest
                        manifestMap.put(file, nullManifest);
                        continue;
                    } else if (! file.contains("/WEB-INF")) {
                        // Don't cache any jars bundled with the app.
                        manifestMap.put(file, manifest);
                    }
                } catch (IOException ex) {
                    // Ignored
                    continue;
                } finally {
                    try {
                        if (jarfile != null)
                            jarfile.close();
                    } catch (IOException ex) {
                        // Ignored
                    }
                }
            } else if (manifest == nullManifest) {
                continue;
            }

            java.util.jar.Attributes attrs = manifest.getMainAttributes();
            String cp = (String) attrs.getValue("Class-Path");
            if (cp == null) {
                continue;
            }

            String[] paths = cp.split(" ");
            int lastIndex = file.lastIndexOf(File.separatorChar);
            String baseDir = "";
            if (lastIndex > 0) {
                baseDir = file.substring(0, lastIndex+1);
            }
            for (String path: paths) {
                String p;
                if (path.startsWith(File.separator)) {
                    p = path;
                } else {
                    p = baseDir + path;
                }
                if (! files.contains(p)) {
                    files.add(p);
                }
            }
        }
        return files;
    }

}


/*******************************************************************************
 * 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.utils;

import java.security.SecureRandom;

import org.eclipse.packagedrone.utils.Strings;

/**
 * Helper methods when working with tokens
 *

 * A token is an array of random bytes, encoded in a lower case hex string. It
 * can be compared to a class 4 UUID, but with a flexible length and no other
 * class variants.
 *

 *

 * Tokens are used for creating salts, or e-mail verification links.
 *

 */
public final class Tokens
{
    private static final SecureRandom random = new SecureRandom ();

    private Tokens ()
    {
    }

    /**
     * Create a random token
     *

     * This method uses the internal instance of of a {@link SecureRandom}
     * generator to create a new token.
     *

     *
     * @param length
     *            the length of the token
     * @return the newly created token, never returns null
     */
    public static String createToken ( final int length )
    {
        final byte[] data = new byte[length];

        random.nextBytes ( data );

        return Strings.hex ( data );
    }
}


/*******************************************************************************
 * Copyright (c) 2014, 2016 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:S2131
 *******************************************************************************/
package org.eclipse.packagedrone.repo.aspect.common.p2;

import static org.osgi.framework.Constants.SINGLETON_DIRECTIVE;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.eclipse.packagedrone.repo.utils.osgi.bundle.BundleInformation;
import org.eclipse.packagedrone.repo.utils.osgi.bundle.BundleInformation.BundleRequirement;
import org.eclipse.packagedrone.repo.utils.osgi.bundle.BundleInformation.PackageExport;
import org.eclipse.packagedrone.repo.utils.osgi.bundle.BundleInformation.PackageImport;
import org.eclipse.packagedrone.repo.utils.osgi.bundle.BundleInformation.VersionRangedName;
import org.eclipse.packagedrone.repo.utils.osgi.feature.FeatureInformation;
import org.eclipse.packagedrone.repo.utils.osgi.feature.FeatureInformation.FeatureInclude;
import org.eclipse.packagedrone.repo.utils.osgi.feature.FeatureInformation.PluginInclude;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class InstallableUnit
{
    private final static Logger logger = LoggerFactory.getLogger ( InstallableUnit.class );

    private static final String DEFAULT_SYSTEM_BUNDLE_ALIAS = "org.eclipse.osgi";

    private String id;

    private Version version;

    private boolean singleton;

    private Map<String, String> properties = new HashMap<> ();

    private String filter;

    private String copyright;

    private String copyrightUrl;

    public static class Artifact
    {
        private final String id;

        private final String classifer;

        private final Version version;

        public Artifact ( final String id, final String classifer, final Version version )
        {
            this.id = id;
            this.classifer = classifer;
            this.version = version;
        }

        public String getId ()
        {
            return this.id;
        }

        public String getClassifer ()
        {
            return this.classifer;
        }

        public Version getVersion ()
        {
            return this.version;
        }
    }

    public static class Entry<T>
    {
        private final String namespace;

        private final String key;

        private final T value;

        public Entry ( final String namespace, final String key, final T value )
        {
            this.namespace = namespace;
            this.key = key;
            this.value = value;
        }

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

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

        public T getValue ()
        {
            return this.value;
        }
    }

    public static class Requirement
    {
        private final VersionRange range;

        private final boolean optional;

        private final Boolean greedy;

        private final String filter;

        public Requirement ( final VersionRange range, final boolean optional, final Boolean greedy, final String filter )
        {
            this.range = range;
            this.optional = optional;
            this.greedy = greedy;
            this.filter = filter;
        }

        public String getFilter ()
        {
            return this.filter;
        }

        public VersionRange getRange ()
        {
            return this.range;
        }

        public boolean isOptional ()
        {
            return this.optional;
        }

        public Boolean getGreedy ()
        {
            return this.greedy;
        }
    }

    public static class License
    {
        private final String text;

        private final String uri;

        public License ( final String text, final String uri )
        {
            this.text = text;
            this.uri = uri;
        }

        public String getText ()
        {
            return this.text;
        }

        public String getUri ()
        {
            return this.uri;
        }
    }

    public static class Touchpoint
    {
        private final String id;

        private final String version;

        private final Map<String, String> instructions;

        public Touchpoint ( final String id, final String version, final Map<String, String> instructions )
        {
            this.id = id;
            this.version = version;
            this.instructions = instructions;
        }

        public String getId ()
        {
            return this.id;
        }

        public String getVersion ()
        {
            return this.version;
        }

        public Map<String, String> getInstructions ()
        {
            return Collections.unmodifiableMap ( this.instructions );
        }
    }

    private List<License> licenses = new LinkedList<> ();

    private List<Entry<Requirement>> requires = new LinkedList<> ();

    private List<Entry<String>> provides = new LinkedList<> ();

    private Set<Artifact> artifacts = new HashSet<> ();

    private List<Touchpoint> touchpoints = new LinkedList<> ();

    public void setLicenses ( final List<License> licenses )
    {
        this.licenses = licenses;
    }

    public List<License> getLicenses ()
    {
        return this.licenses;
    }

    public void setCopyright ( final String copyright )
    {
        this.copyright = copyright;
    }

    public void setCopyright ( final String copyright, final String copyrightUrl )
    {
        this.copyright = copyright;
        this.copyrightUrl = copyrightUrl;
    }

    public void setCopyrightUrl ( final String copyrightUrl )
    {
        this.copyrightUrl = copyrightUrl;
    }

    public void setArtifacts ( final Set<Artifact> artifacts )
    {
        this.artifacts = artifacts;
    }

    public Set<Artifact> getArtifacts ()
    {
        return this.artifacts;
    }

    public void setTouchpoints ( final List<Touchpoint> touchpoints )
    {
        this.touchpoints = touchpoints;
    }

    public List<Touchpoint> getTouchpoints ()
    {
        return this.touchpoints;
    }

    public void setFilter ( final String filter )
    {
        this.filter = filter;
    }

    public String getFilter ()
    {
        return this.filter;
    }

    public void setRequires ( final List<Entry<Requirement>> requires )
    {
        this.requires = requires;
    }

    public List<Entry<Requirement>> getRequires ()
    {
        return this.requires;
    }

    public void setProvides ( final List<Entry<String>> provides )
    {
        this.provides = provides;
    }

    public List<Entry<String>> getProvides ()
    {
        return this.provides;
    }

    public void setProperties ( final Map<String, String> properties )
    {
        this.properties = properties;
    }

    public Map<String, String> getProperties ()
    {
        return this.properties;
    }

    public void setSingleton ( final boolean singleton )
    {
        this.singleton = singleton;
    }

    public boolean isSingleton ()
    {
        return this.singleton;
    }

    public void setId ( final String id )
    {
        this.id = id;
    }

    public String getId ()
    {
        return this.id;
    }

    public void setVersion ( final Version version )
    {
        this.version = version;
    }

    public Version getVersion ()
    {
        return this.version;
    }

    public static List<InstallableUnit> fromFeature ( final FeatureInformation feature )
    {
        final List<InstallableUnit> result = new ArrayList<> ( 2 );

        result.add ( createFeatureGroup ( feature ) );
        result.add ( createFeatureJar ( feature ) );

        return result;
    }

    private static InstallableUnit createFeatureJar ( final FeatureInformation feature )
    {
        final InstallableUnit result = new InstallableUnit ();

        // core

        result.setId ( feature.getId () + ".feature.jar" );
        result.setVersion ( feature.getVersion () );

        // properties

        final Map<String, String> props = result.getProperties ();
        props.put ( "org.eclipse.equinox.p2.name", feature.getLabel () );
        addProperty ( props, "org.eclipse.equinox.p2.provider", feature.getProvider () );
        addProperty ( props, "org.eclipse.equinox.p2.description", feature.getDescription () );
        addProperty ( props, "org.eclipse.equinox.p2.description.url", feature.getDescriptionUrl () );
        addProperty ( props, "org.eclipse.update.feature.plugin", feature.getPlugin () );

        // localization

        addLocalization ( props, result.getProvides (), feature.getLocalization () );

        // provides

        addProvides ( result, "org.eclipse.equinox.p2.iu", feature.getId () + ".feature.jar", "" + feature.getVersion () );
        addProvides ( result, "org.eclipse.update.feature", feature.getId (), "" + feature.getVersion () );
        addProvides ( result, "org.eclipse.equinox.p2.eclipse.type", "feature", "1.0.0" );

        // filter

        result.setFilter ( "(org.eclipse.update.install.features=true)" );

        // artifacts

        result.getArtifacts ().add ( new Artifact ( feature.getId (), "org.eclipse.update.feature", feature.getVersion () ) );

        // legal

        result.setCopyright ( feature.getCopyright (), feature.getCopyrightUrl () );
        result.getLicenses ().add ( new License ( feature.getLicense (), feature.getLicenseUrl () ) );

        // touchpoint

        final Map<String, String> td = new HashMap<> ();
        td.put ( "zipped", "true" );
        result.getTouchpoints ().add ( new Touchpoint ( "org.eclipse.equinox.p2.osgi", "1.0.0", td ) );

        return result;
    }

    private static InstallableUnit createFeatureGroup ( final FeatureInformation feature )
    {
        logger.debug ( "Processing: {}", feature );

        final InstallableUnit result = new InstallableUnit ();

        // version

        final Version version = feature.getVersion ();
        if ( version == null )
        {
            return null;
        }

        // core

        result.setId ( feature.getId () + ".feature.group" );
        result.setVersion ( version );

        // provides

        addProvides ( result, "org.eclipse.equinox.p2.iu", feature.getId () + ".feature.group", "" + feature.getVersion () );

        // properties

        final Map<String, String> props = result.getProperties ();
        addProperty ( props, "org.eclipse.equinox.p2.name", feature.getLabel () );
        props.put ( "org.eclipse.equinox.p2.type.group", "true" );
        addProperty ( props, "org.eclipse.equinox.p2.provider", feature.getProvider () );
        addProperty ( props, "org.eclipse.equinox.p2.description", feature.getDescription () );
        addProperty ( props, "org.eclipse.equinox.p2.description.url", feature.getDescriptionUrl () );

        // localization

        addLocalization ( props, result.getProvides (), feature.getLocalization () );

        // requirements

        final List<Entry<Requirement>> reqs = result.getRequires ();

        final String versionRange = String.format ( "[%1$s,%1$s]", version );
        logger.debug ( "VersionRange: {}", versionRange );

        addRequires ( reqs, "org.eclipse.equinox.p2.iu", feature.getId () + ".feature.jar", new Requirement ( new VersionRange ( versionRange ), false, null, "(org.eclipse.update.install.features=true)" ) );

        for ( final FeatureInclude fi : feature.getIncludedFeatures () )
        {
            final String filter = fi.getQualifiers ().toFilterString ();
            final VersionRange range = fi.makeVersionRange ();
            addRequires ( reqs, "org.eclipse.equinox.p2.iu", fi.getId () + ".feature.group", new Requirement ( range, fi.isOptional (), null, filter ) );
        }

        for ( final PluginInclude pi : feature.getIncludedPlugins () )
        {
            final String filter = pi.getQualifiers ().toFilterString ();
            final VersionRange range = pi.makeVersionRange ();
            addRequires ( reqs, "org.eclipse.equinox.p2.iu", pi.getId (), new Requirement ( range, false, null, filter ) );
        }

        for ( final org.eclipse.packagedrone.repo.utils.osgi.feature.FeatureInformation.Requirement ri : feature.getRequirements () )
        {
            final String id;
            switch ( ri.getType () )
            {
                case FEATURE:
                    id = ri.getId () + ".feature.group";
                    break;
                case PLUGIN:
                    id = ri.getId ();
                    break;
                default:
                    throw new IllegalStateException ( String.format ( "Feature requirement type %s is unknown", ri.getType () ) );
            }

            Version v = ri.getVersion ();
            if ( v == null )
            {
                // fall back to empty version
                v = Version.emptyVersion;
            }

            addRequires ( reqs, "org.eclipse.equinox.p2.iu", id, new Requirement ( ri.getMatchRule ().makeRange ( v ), false, null, null ) );
        }

        // filter

        result.setFilter ( feature.getQualifiers ().toFilterString () );

        // legal

        result.setCopyright ( feature.getCopyright (), feature.getCopyrightUrl () );
        result.getLicenses ().add ( new License ( feature.getLicense (), feature.getLicenseUrl () ) );

        // touchpoint - no touchpoints

        // return result

        return result;
    }

    public static InstallableUnit fromBundle ( final BundleInformation bundle )
    {
        return fromBundle ( bundle, new P2MetaDataInformation () );
    }

    public static InstallableUnit fromBundle ( final BundleInformation bundle, final P2MetaDataInformation info )
    {
        final InstallableUnit result = new InstallableUnit ();

        // core

        result.setId ( bundle.getId () );
        result.setVersion ( bundle.getVersion () );
        result.setSingleton ( bundle.isSingleton () );

        // properties

        final Map<String, String> props = result.getProperties ();
        addProperty ( props, "org.eclipse.equinox.p2.name", bundle.getName () );
        addProperty ( props, "org.eclipse.equinox.p2.provider", bundle.getVendor () );
        addProperty ( props, "org.eclipse.equinox.p2.bundle.localization", bundle.getBundleLocalization () );

        // provides

        addProvides ( result, "osgi.bundle", bundle.getId (), "" + bundle.getVersion () );
        addProvides ( result, "org.eclipse.equinox.p2.iu", bundle.getId (), "" + bundle.getVersion () );
        if ( bundle.isSourceBundle () )
        {
            addProvides ( result, "org.eclipse.equinox.p2.eclipse.type", "source", "1.0.0" );
        }
        else
        {
            addProvides ( result, "org.eclipse.equinox.p2.eclipse.type", "bundle", "1.0.0" );
        }
        for ( final PackageExport pe : bundle.getPackageExports () )
        {
            addProvides ( result, "java.package", pe.getName (), makeVersion ( pe.getVersion () ) );
        }

        if ( bundle.getFragmentHost () != null )
        {
            addProvides ( result, "osgi.fragment", bundle.getFragmentHost ().getName (), makeVersion ( bundle.getVersion () ) );
        }

        // localization

        addLocalization ( props, result.getProvides (), bundle.getLocalization () );

        // requirements

        for ( final PackageImport pi : bundle.getPackageImports () )
        {
            addRequires ( result, "java.package", pi.getName (), new Requirement ( pi.getVersionRange (), pi.isOptional (), pi.isOptional () ? false : null, null ) );
        }

        final String systemBundleAlias = getSystemBundleAlias ( info );

        for ( final BundleRequirement br : bundle.getBundleRequirements () )
        {
            addRequires ( result, "osgi.bundle", transformBundleName ( systemBundleAlias, br.getId () ), new Requirement ( br.getVersionRange (), br.isOptional (), br.isOptional () ? false : null, null ) );
        }

        final VersionRangedName fragmentHost = bundle.getFragmentHost ();
        if ( fragmentHost != null )
        {
            VersionRange versionRange = fragmentHost.getVersionRange ();
            versionRange = versionRange != null ? versionRange : VersionRange.valueOf ( "0.0.0" );
            addRequires ( result, "osgi.bundle", transformBundleName ( systemBundleAlias, fragmentHost.getName () ), new Requirement ( versionRange, false, null, null ) );
        }
        // artifacts

        result.getArtifacts ().add ( new Artifact ( bundle.getId (), "osgi.bundle", bundle.getVersion () ) );

        // touchpoints

        try
        {
            final Map<String, String> td = new HashMap<> ( 1 );
            td.put ( "manifest", makeManifest ( bundle ) );
            if ( "dir".equals ( bundle.getEclipseBundleShape () ) )
            {
                td.put ( "zipped", "true" );
            }
            result.getTouchpoints ().add ( new Touchpoint ( "org.eclipse.equinox.p2.osgi", "1.0.0", td ) );
        }
        catch ( final Exception e )
        {
            logger.warn ( "Failed to generate manifest", e );
        }

        result.setFilter ( bundle.getEclipsePlatformFilter () );

        return result;
    }

    private static String getSystemBundleAlias ( final P2MetaDataInformation info )
    {
        if ( info.getSystemBundleAlias () != null && !info.getSystemBundleAlias ().isEmpty () )
        {
            return info.getSystemBundleAlias ();
        }
        else
        {
            return DEFAULT_SYSTEM_BUNDLE_ALIAS;
        }
    }

    private static String transformBundleName ( final String systemBundleAlias, final String id )
    {
        if ( id.equals ( Constants.SYSTEM_BUNDLE_SYMBOLICNAME ) )
        {
            return systemBundleAlias;
        }
        else
        {
            return id;
        }
    }

    /**
     * Add property entry only of value is not null
     *
     * @param props
     *            properties
     * @param key
     *            to add
     * @param value
     *            to add
     */
    private static void addProperty ( final Map<String, String> props, final String key, final String value )
    {
        if ( value == null )
        {
            return;
        }

        props.put ( key, value );
    }

    private static void addProvides ( final InstallableUnit result, final String namespace, final String key, final String value )
    {
        addProvides ( result.getProvides (), namespace, key, value );
    }

    private static void addProvides ( final List<Entry<String>> provides, final String namespace, final String key, final String value )
    {
        provides.add ( new Entry<> ( namespace, key, value ) );
    }

    private static void addRequires ( final InstallableUnit result, final String namespace, final String key, final Requirement value )
    {
        addRequires ( result.getRequires (), namespace, key, value );
    }

    private static void addRequires ( final List<Entry<Requirement>> requires, final String namespace, final String key, final Requirement value )
    {
        requires.add ( new Entry<> ( namespace, key, value ) );
    }

    private static void addLocalization ( final Map<String, String> properties, final List<Entry<String>> provides, final Map<String, Properties> localization )
    {
        for ( final String loc : localization.keySet () )
        {
            addProvides ( provides, "org.eclipse.equinox.p2.localization", loc, "1.0.0" );
        }

        for ( final Map.Entry<String, Properties> le : localization.entrySet () )
        {
            final String locale = le.getKey ();
            for ( final Map.Entry<Object, Object> pe : le.getValue ().entrySet () )
            {
                properties.put ( locale + "." + pe.getKey (), "" + pe.getValue () );
            }
        }
    }

    private static String makeVersion ( final Version version )
    {
        if ( version == null )
        {
            return "0.0.0";
        }
        return version.toString ();
    }

    /**
     * Write a list of units inside a {@code units} element
     *
     * @param xsw
     *            the stream to write to
     * @param ius
     *            The units to write
     * @throws XMLStreamException
     *             if the underlying throws it
     */
    public static void writeXml ( final XMLStreamWriter xsw, final List<InstallableUnit> ius ) throws XMLStreamException
    {
        xsw.writeStartElement ( "units" );
        xsw.writeAttribute ( "size", Integer.toString ( ius.size () ) );
        for ( final InstallableUnit iu : ius )
        {
            iu.writeXmlForUnit ( xsw );
        }
        xsw.writeEndElement ();
    }

    /**
     * Write a single unit inside a {@code units} element
     *
     * @param xsw
     *            the stream to write to
     * @throws XMLStreamException
     *             if the underlying throws it
     */
    public void writeXml ( final XMLStreamWriter xsw ) throws XMLStreamException
    {
        writeXml ( xsw, Collections.singletonList ( this ) );
    }

    /**
     * Write the unit as XML fragment
     *
     * @param xsw
     *            the XMLStreamWriter to write to
     * @throws XMLStreamException
     *             if the underlying throws it
     */
    public void writeXmlForUnit ( final XMLStreamWriter xsw ) throws XMLStreamException
    {
        xsw.writeStartElement ( "unit" );
        xsw.writeAttribute ( "id", this.id );
        xsw.writeAttribute ( "version", "" + this.version );
        xsw.writeAttribute ( "singleton", Boolean.toString ( this.singleton ) );

        {
            xsw.writeEmptyElement ( "update" );
            xsw.writeAttribute ( "id", this.id );
            xsw.writeAttribute ( "range", "[0.0.0," + this.version + ")" );
            xsw.writeAttribute ( "severity", "0" );
        }

        {
            xsw.writeStartElement ( "properties" );
            xsw.writeAttribute ( "size", Integer.toString ( this.properties.size () ) );

            for ( final Map.Entry<String, String> entry : this.properties.entrySet () )
            {
                xsw.writeStartElement ( "property" );
                xsw.writeAttribute ( "name", entry.getKey () );
                if ( entry.getValue () != null )
                {
                    xsw.writeAttribute ( "value", entry.getValue () );
                }
                xsw.writeEndElement ();
            }

            xsw.writeEndElement (); // properties
        }

        {
            xsw.writeStartElement ( "provides" );
            xsw.writeAttribute ( "size", Integer.toString ( this.provides.size () ) );

            for ( final Entry<String> entry : this.provides )
            {
                xsw.writeStartElement ( "provided" );
                xsw.writeAttribute ( "namespace", entry.getNamespace () );
                xsw.writeAttribute ( "name", entry.getKey () );
                xsw.writeAttribute ( "version", entry.getValue () );
                xsw.writeEndElement ();
            }

            xsw.writeEndElement (); // provides
        }

        {
            xsw.writeStartElement ( "requires" );
            xsw.writeAttribute ( "size", Integer.toString ( this.requires.size () ) );

            for ( final Entry<Requirement> entry : this.requires )
            {
                xsw.writeStartElement ( "required" );

                xsw.writeAttribute ( "namespace", entry.getNamespace () );
                xsw.writeAttribute ( "name", entry.getKey () );
                xsw.writeAttribute ( "range", makeString ( entry.getValue ().getRange () ) );

                if ( entry.getValue ().isOptional () )
                {
                    xsw.writeAttribute ( "optional", "true" );

                }
                if ( entry.getValue ().getGreedy () != null )
                {
                    xsw.writeAttribute ( "greedy", "" + entry.getValue ().getGreedy () );
                }

                final String filterString = entry.getValue ().getFilter ();
                if ( filterString != null && !filterString.isEmpty () )
                {
                    xsw.writeStartElement ( "filter" );
                    xsw.writeCharacters ( entry.getValue ().getFilter () );
                    xsw.writeEndElement (); // filter
                }

                xsw.writeEndElement (); // required
            }

            xsw.writeEndElement (); // requires
        }

        {
            if ( this.filter != null && !this.filter.isEmpty () )
            {
                xsw.writeStartElement ( "filter" );
                xsw.writeCharacters ( this.filter );
                xsw.writeEndElement (); // filter
            }
        }

        if ( !this.artifacts.isEmpty () )
        {
            xsw.writeStartElement ( "artifacts" );
            xsw.writeAttribute ( "size", Integer.toString ( this.artifacts.size () ) );

            for ( final Artifact artifact : this.artifacts )
            {
                xsw.writeEmptyElement ( "artifact" );
                xsw.writeAttribute ( "classifier", artifact.getClassifer () );
                xsw.writeAttribute ( "id", artifact.getId () );
                xsw.writeAttribute ( "version", "" + artifact.getVersion () );
            }

            xsw.writeEndElement (); // artifacts
        }

        {
            if ( this.touchpoints.isEmpty () )
            {
                xsw.writeEmptyElement ( "touchpoint" );
                xsw.writeAttribute ( "id", "null" );
                xsw.writeAttribute ( "version", "0.0.0" );
            }
            else
            {
                for ( final Touchpoint tp : this.touchpoints )
                {
                    xsw.writeEmptyElement ( "touchpoint" );
                    xsw.writeAttribute ( "id", tp.getId () );
                    xsw.writeAttribute ( "version", tp.getVersion () );

                    if ( !tp.getInstructions ().isEmpty () )
                    {
                        xsw.writeStartElement ( "touchpointData" );
                        xsw.writeAttribute ( "size", "1" );

                        xsw.writeStartElement ( "instructions" );
                        xsw.writeAttribute ( "size", Integer.toString ( tp.getInstructions ().size () ) );

                        for ( final Map.Entry<String, String> entry : tp.getInstructions ().entrySet () )
                        {
                            xsw.writeStartElement ( "instruction" );
                            xsw.writeAttribute ( "key", entry.getKey () );
                            xsw.writeCharacters ( entry.getValue () );
                            xsw.writeEndElement (); // instruction
                        }

                        xsw.writeEndElement (); // instructions
                        xsw.writeEndElement (); // touchpointData
                    }
                }
            }
        }

        {
            xsw.writeStartElement ( "licenses" );
            xsw.writeAttribute ( "size", Integer.toString ( this.licenses.size () ) );

            for ( final License licenseEntry : this.licenses )
            {
                xsw.writeStartElement ( "license" );

                if ( licenseEntry.getUri () != null )
                {
                    xsw.writeAttribute ( "url", licenseEntry.getUri () );
                    xsw.writeAttribute ( "uri", licenseEntry.getUri () );
                }

                if ( licenseEntry.getText () != null )
                {
                    xsw.writeCData ( licenseEntry.getText () );
                }

                xsw.writeEndElement (); // license
            }

            xsw.writeEndElement (); // licenses
        }

        if ( this.copyright != null || this.copyrightUrl != null )
        {
            xsw.writeStartElement ( "copyright" );

            if ( this.copyrightUrl != null )
            {
                xsw.writeAttribute ( "url", this.copyrightUrl );
                xsw.writeAttribute ( "uri", this.copyrightUrl );
            }
            if ( this.copyright != null )
            {
                xsw.writeCData ( this.copyright );
            }
            xsw.writeEndElement ();
        }

        xsw.writeEndElement (); // unit
    }

    private String makeString ( final VersionRange range )
    {
        if ( range == null )
        {
            return "0.0.0";
        }
        else
        {
            return range.toString ();
        }
    }

    private static String makeManifest ( final BundleInformation bundle ) throws IOException
    {
        final Manifest mf = new Manifest ();
        mf.getMainAttributes ().put ( Attributes.Name.MANIFEST_VERSION, "1.0" );
        mf.getMainAttributes ().putValue ( Constants.BUNDLE_SYMBOLICNAME, bundle.getId () + ( bundle.isSingleton () ? ";" + SINGLETON_DIRECTIVE + ":=true" : "" ) );
        mf.getMainAttributes ().putValue ( Constants.BUNDLE_VERSION, "" + bundle.getVersion () );

        final VersionRangedName fragmentHost = bundle.getFragmentHost ();
        if ( fragmentHost != null && fragmentHost.getName () != null )
        {
            final String name = fragmentHost.getName ();
            final Optional<VersionRange> version = Optional.ofNullable ( fragmentHost.getVersionRange () );
            final Optional<String> versionStr = version.map ( vr -> ";" + Constants.BUNDLE_VERSION_ATTRIBUTE + "=\"" + vr.toString () + "\"" );
            mf.getMainAttributes ().putValue ( Constants.FRAGMENT_HOST, name + versionStr.orElse ( "" ) );
        }

        final ByteArrayOutputStream out = new ByteArrayOutputStream ();
        mf.write ( out );
        out.close ();
        return out.toString ( "UTF-8" );
    }

}


/*******************************************************************************
 * 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.job.apm.model;

import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.packagedrone.job.State;

/**
 * The write model of the job storage
 */
public class JobWriteModel
{
    private static final Duration JOB_FORGET_DURATION = Duration.ofHours ( 8 );

    private final Map<String, JobInstanceEntity> jobs;

    private final Map<String, JobInstanceEntity> cloneMap = new HashMap<> ();

    public JobWriteModel ()
    {
        this.jobs = new HashMap<> ();
    }

    public JobWriteModel ( final Map<String, JobInstanceEntity> jobs )
    {
        this.jobs = jobs;
    }

    /**
     * Get a job instance for updating it.
     *

     * The fetched job will automatically be cloned and persisted.
     *

     *
     * @param id
     *            the id of the job
     * @return the job instance entity or null if none could be
     *         found
     */
    public JobInstanceEntity getJobForUpdate ( final String id )
    {
        JobInstanceEntity job = this.cloneMap.get ( id );

        if ( job != null )
        {
            // this already is the cloned job
            return job;
        }

        // fetch the job
        job = this.jobs.get ( id );
        if ( job == null )
        {
            return null;
        }

        // make the clone
        final JobInstanceEntity clonedJob = new JobInstanceEntity ( job );

        this.cloneMap.put ( id, clonedJob );

        return clonedJob;
    }

    public Map<String, JobInstanceEntity> makeJobMap ()
    {
        final Map<String, JobInstanceEntity> result = new HashMap<> ( this.jobs );

        final Instant timedOut = Instant.now ().minus ( JOB_FORGET_DURATION );

        cloneActiveJobs ( this.jobs.values (), result, timedOut );
        cloneActiveJobs ( this.cloneMap.values (), result, timedOut );

        return result;
    }

    protected static void cloneActiveJobs ( final Collection<JobInstanceEntity> jobs, final Map<String, JobInstanceEntity> result, final Instant timedOut )
    {
        jobs.stream ().filter ( job -> job.getState () != State.COMPLETE || job.getCreation ().isAfter ( timedOut ) ).forEach ( job -> result.put ( job.getId (), job ) );
    }

    public void addJob ( JobInstanceEntity ji )
    {
        ji = new JobInstanceEntity ( ji );

        if ( ji.getId () == null )
        {
            throw new NullPointerException ( "Job ID must not be null" );
        }

        this.cloneMap.put ( ji.getId (), ji );
    }

}