/*******************************************************************************
 * 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
 *     M-Ezzat - code cleanup - squid:S2162
 *******************************************************************************/

package org.eclipse.packagedrone.repo.channel.web.channel;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.packagedrone.repo.ChannelAspectInformation;
import org.eclipse.packagedrone.repo.Version;
import org.eclipse.packagedrone.repo.aspect.group.GroupInformation;

/**
 * More UI suited channel aspect information
 */
public class AspectInformation
{
    private static final Comparator<AspectInformation> NAME_COMPARATOR = new Comparator<AspectInformation> () {

        @Override
        public int compare ( final AspectInformation o1, final AspectInformation o2 )
        {
            final int rc = o1.getName ().compareTo ( o2.getName () );
            if ( rc != 0 )
            {
                return rc;
            }

            return o1.getFactoryId ().compareTo ( o2.getFactoryId () );
        }
    };

    private Group group;

    private final ChannelAspectInformation information;

    private List<AspectInformation> requires = Collections.emptyList ();

    public AspectInformation ( final ChannelAspectInformation information )
    {
        this.information = information;
    }

    public String getName ()
    {
        return this.information.getLabel ();
    }

    public String getFactoryId ()
    {
        return this.information.getFactoryId ();
    }

    public ChannelAspectInformation getInformation ()
    {
        return this.information;
    }

    public boolean isResolved ()
    {
        return this.information.isResolved ();
    }

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

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

    public Group getGroup ()
    {
        return this.group;
    }

    public static List<AspectInformation> resolve ( final Collection<GroupInformation> groups, final Collection<ChannelAspectInformation> aspects )
    {
        if ( aspects == null )
        {
            return null;
        }

        final Map<String, AspectInformation> map = new HashMap<> ( aspects.size () );

        // convert

        for ( final ChannelAspectInformation aspect : aspects )
        {
            map.put ( aspect.getFactoryId (), new AspectInformation ( aspect ) );
        }

        // convert groups

        final Map<String, Group> groupMap = new HashMap<> ( groups.size () );
        for ( final GroupInformation gi : groups )
        {
            groupMap.put ( gi.getId (), new Group ( gi.getId (), gi.getName () ) );
        }

        // then resolve dependencies and assign groups

        final List<AspectInformation> result = new ArrayList<> ( aspects.size () );

        for ( final AspectInformation info : map.values () )
        {
            info.resolveDeps ( map );
            info.group = groupMap.get ( info.information.getGroupId () );
            if ( info.group == null )
            {
                info.group = Group.OTHER;
            }
            result.add ( info );
        }

        // sort

        Collections.sort ( result, NAME_COMPARATOR );

        return result;

    }

    private void resolveDeps ( final Map<String, AspectInformation> map )
    {
        if ( this.information.getRequires () == null )
        {
            this.requires = Collections.emptyList ();
            return;
        }

        this.requires = new ArrayList<> ( this.information.getRequires ().size () );
        for ( final String req : this.information.getRequires () )
        {
            AspectInformation reqInfo = map.get ( req );
            if ( reqInfo == null )
            {
                reqInfo = new AspectInformation ( ChannelAspectInformation.unresolved ( req ) );
            }
            this.requires.add ( reqInfo );
        }

        Collections.sort ( this.requires, NAME_COMPARATOR );
    }

    /**
     * Filter the provided aspect lists by a predicate on the ID
     *
     * @param list
     *            the list to filter
     * @param predicate
     *            the ID predicate
     * @return the filtered list, returns only null when the list
     *         was null
     */
    public static List<AspectInformation> filterIds ( final List<AspectInformation> list, final Predicate<String> predicate )
    {
        if ( list == null )
        {
            return null;
        }

        return list.stream ().filter ( ( i ) -> predicate.test ( i.getFactoryId () ) ).collect ( Collectors.toList () );
    }

    /**
     * Get all aspect ids which are currently missing but required by this
     * aspect
     *
     * @param assignedAspects
     *            the aspects which are considered assigned
     * @return A possibly empty, array of all missing but required aspects.
     *         Never returns null
     */
    public String[] getMissingIds ( final List<AspectInformation> assignedAspects )
    {
        final Set<AspectInformation> required = new HashSet<> ();

        addRequired ( required, this, assignedAspects );

        return required.stream ().map ( AspectInformation::getFactoryId ).toArray ( size -> new String[size] );
    }

    private static void addRequired ( final Set<AspectInformation> result, final AspectInformation aspect, final List<AspectInformation> assignedAspects )
    {
        for ( final AspectInformation req : aspect.getRequires () )
        {
            if ( !assignedAspects.contains ( req ) && result.add ( req ) )
            {
                addRequired ( result, req, assignedAspects );
            }
        }
    }

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

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

    public static class Group implements Comparable<Group>
    {
        public static final Group OTHER = new Group ( "other", "Other" );

        private final String id;

        private final String name;

        public Group ( final String id, final String name )
        {
            this.id = id;
            this.name = name;
        }

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

        public String getName ()
        {
            return this.name;
        }

        @Override
        public int compareTo ( final Group o )
        {
            final int rc = this.name.compareTo ( o.name );
            if ( rc != 0 )
            {
                return rc;
            }

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

        @Override
        public String toString ()
        {
            return this.id;
        }

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

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

    public static Map<Group, List<AspectInformation>> group ( final List<AspectInformation> aspects )
    {
        final Map<Group, List<AspectInformation>> result = new HashMap<> ();

        for ( final AspectInformation ai : aspects )
        {
            List<AspectInformation> list = result.get ( ai.getGroup () );
            if ( list == null )
            {
                list = new LinkedList<> ();
                result.put ( ai.getGroup (), list );
            }
            list.add ( ai );
        }

        return result;
    }

    @Override
    public String toString ()
    {
        return String.format ( "[%s: %s]", this.information, this.group );
    }

}


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

package org.apache.jasper.xmlparser;

import java.io.*;
import java.net.URI;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import java.util.logging.Level;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.XMLConstants;

import org.apache.jasper.Constants;
import org.apache.jasper.JasperException;
import org.apache.jasper.compiler.Localizer;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.w3c.dom.ls.LSResourceResolver;
import org.w3c.dom.ls.LSInput;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * XML parsing utilities for processing web application deployment
 * descriptor and tag library descriptor files.  FIXME - make these
 * use a separate class loader for the parser to be used.
 *
 * @author Craig R. McClanahan
 * @version $Revision: 1.11 $ $Date: 2007/05/05 05:32:59 $
 */

public class ParserUtils {

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

    /**
     * An error handler for use when parsing XML documents.
     */
    private static ErrorHandler errorHandler = new MyErrorHandler();

    /**
     * An entity resolver for use when parsing XML documents.
     */
    static EntityResolver entityResolver = new MyEntityResolver();

    static String schemaResourcePrefix;

    static String dtdResourcePrefix;

    static boolean isDtdResourcePrefixFileUrl = false;
    static boolean isSchemaResourcePrefixFileUrl = false;

    private static final String SCHEMA_LOCATION_ATTR = "schemaLocation";

    private static HashMap<String, Schema> schemaCache =
        new HashMap<String, Schema>();

    /**
     * List of the Public IDs that we cache, and their
     * associated location. This is used by
     * an EntityResolver to return the location of the
     * cached copy of a DTD.
     */
    static final String[] CACHED_DTD_PUBLIC_IDS = {
        Constants.TAGLIB_DTD_PUBLIC_ID_11,
        Constants.TAGLIB_DTD_PUBLIC_ID_12,
        Constants.WEBAPP_DTD_PUBLIC_ID_22,
        Constants.WEBAPP_DTD_PUBLIC_ID_23,
    };

    // START PWC 6386258
    private static final String[] DEFAULT_DTD_RESOURCE_PATHS = {
        Constants.TAGLIB_DTD_RESOURCE_PATH_11,
        Constants.TAGLIB_DTD_RESOURCE_PATH_12,
        Constants.WEBAPP_DTD_RESOURCE_PATH_22,
        Constants.WEBAPP_DTD_RESOURCE_PATH_23,
    };

    static final String[] CACHED_DTD_RESOURCE_PATHS =
            (String[])DEFAULT_DTD_RESOURCE_PATHS;

    private static final String[] DEFAULT_SCHEMA_RESOURCE_PATHS = {
        Constants.TAGLIB_SCHEMA_RESOURCE_PATH_20,
        Constants.TAGLIB_SCHEMA_RESOURCE_PATH_21,
        Constants.WEBAPP_SCHEMA_RESOURCE_PATH_24,
        Constants.WEBAPP_SCHEMA_RESOURCE_PATH_25,
    };

    static final String[] CACHED_SCHEMA_RESOURCE_PATHS =
            (String[])DEFAULT_SCHEMA_RESOURCE_PATHS;
    // END PWC 6386258

    // --------------------------------------------------------- Static Methods

    public static void setEntityResolver(EntityResolver er) {
        entityResolver = er;
    }

    // START PWC 6386258
    /**
     * Sets the path prefix URL for .xsd resources
     */
    public static void setSchemaResourcePrefix(String prefix) {

        if (prefix != null && prefix.startsWith("file:")) {
            schemaResourcePrefix = uencode(prefix);
            isSchemaResourcePrefixFileUrl = true;
        } else {
            schemaResourcePrefix = prefix;
            isSchemaResourcePrefixFileUrl = false;
        }

        for (int i=0; i<CACHED_SCHEMA_RESOURCE_PATHS.length; i++) {
            String path = DEFAULT_SCHEMA_RESOURCE_PATHS[i];
            int index = path.lastIndexOf('/');
            if (index != -1) {
                CACHED_SCHEMA_RESOURCE_PATHS[i] =
                    schemaResourcePrefix + path.substring(index+1);
            }
        }
    }

    /**
     * Sets the path prefix URL for .dtd resources
     */
    public static void setDtdResourcePrefix(String prefix) {

        if (prefix != null && prefix.startsWith("file:")) {
            dtdResourcePrefix = uencode(prefix);
            isDtdResourcePrefixFileUrl = true;
        } else {
            dtdResourcePrefix = prefix;
            isDtdResourcePrefixFileUrl = false;
        }

        for (int i=0; i<CACHED_DTD_RESOURCE_PATHS.length; i++) {
            String path = DEFAULT_DTD_RESOURCE_PATHS[i];
            int index = path.lastIndexOf('/');
            if (index != -1) {
                CACHED_DTD_RESOURCE_PATHS[i] =
                    dtdResourcePrefix + path.substring(index+1);
            }
        }
    }
    // END PWC 6386258

    private static String uencode(String prefix) {
        if (prefix != null && prefix.startsWith("file:")) {
            StringTokenizer tokens = new StringTokenizer(prefix, "/\\:", true);
            StringBuilder stringBuilder = new StringBuilder();
            while (tokens.hasMoreElements()) {
                String token = tokens.nextToken();
                if ("/".equals(token) || "\\".equals(token) || ":".equals(token)) {
                    stringBuilder.append(token);
                } else {
                    try {
                        stringBuilder.append(URLEncoder.encode(token, "UTF-8"));
                    } catch(java.io.UnsupportedEncodingException ex) {
                    }
                }
            }

            return stringBuilder.toString();
        } else {
            return prefix;
        }
    }

    // --------------------------------------------------------- Public Methods

    /**
     * Parse the specified XML document, and return a TreeNode
     * that corresponds to the root node of the document tree.
     *
     * @param uri URI of the XML document being parsed
     * @param is Input source containing the deployment descriptor
     *
     * @exception JasperException if an I/O or parsing error has occurred
     */
    public TreeNode parseXMLDocument(String uri, InputSource is)
            throws JasperException {
        return parseXMLDocument(uri, is, false);
    }

    /**
     * Parse the specified XML document, and return a TreeNode
     * that corresponds to the root node of the document tree.
     *
     * @param uri URI of the XML document being parsed
     * @param is Input source containing the deployment descriptor
     * @param validate true if the XML document needs to be validated against
     * its DTD or schema, false otherwise
     *
     * @exception JasperException if an I/O or parsing error has occurred
     */
    public TreeNode parseXMLDocument(String uri, InputSource is,
                                     boolean validate)
            throws JasperException {

        Document document = null;

        // Perform an XML parse of this document, via JAXP

        // START 6412405
        ClassLoader currentLoader =
            Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(
            getClass().getClassLoader());
        // END 6412405
        try {
            DocumentBuilderFactory factory =
                DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            /* See CR 6399139
            factory.setFeature(
                "http://apache.org/xml/features/validation/dynamic",
                true);
            */
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setEntityResolver(entityResolver);
            builder.setErrorHandler(errorHandler);
            document = builder.parse(is);
            document.setDocumentURI(uri);
            if (validate) {
                Schema schema = getSchema(document);
                if (schema != null) {
                    // Validate TLD against specified schema
                    schema.newValidator().validate(new DOMSource(document));
                }
                /* See CR 6399139
                else {
                    log.warning(Localizer.getMessage(
                        "jsp.warning.dtdValidationNotSupported"));
                }
                */
            }
	} catch (ParserConfigurationException ex) {
            throw new JasperException
                (Localizer.getMessage("jsp.error.parse.xml", uri), ex);
	} catch (SAXParseException ex) {
            throw new JasperException
                (Localizer.getMessage("jsp.error.parse.xml.line",
				      uri,
				      Integer.toString(ex.getLineNumber()),
				      Integer.toString(ex.getColumnNumber())),
		 ex);
	} catch (SAXException sx) {
            throw new JasperException
                (Localizer.getMessage("jsp.error.parse.xml", uri), sx);
        } catch (IOException io) {
            throw new JasperException
                (Localizer.getMessage("jsp.error.parse.xml", uri), io);
        // START 6412405
        } finally {
            Thread.currentThread().setContextClassLoader(currentLoader);
        }
        // END 6412405

        // Convert the resulting document to a graph of TreeNodes
        return (convert(null, document.getDocumentElement()));
    }

    /**
     * Parse the specified XML document, and return a TreeNode
     * that corresponds to the root node of the document tree.
     *
     * @param uri URI of the XML document being parsed
     * @param is Input stream containing the deployment descriptor
     *
     * @exception JasperException if an I/O or parsing error has occurred
     */
    public TreeNode parseXMLDocument(String uri, InputStream is)
            throws JasperException {
        return parseXMLDocument(uri, new InputSource(is), false);
    }

    /**
     * Parse the specified XML document, and return a TreeNode
     * that corresponds to the root node of the document tree.
     *
     * @param uri URI of the XML document being parsed
     * @param is Input stream containing the deployment descriptor
     * @param validate true if the XML document needs to be validated against
     * its DTD or schema, false otherwise
     *
     * @exception JasperException if an I/O or parsing error has occurred
     */
    public TreeNode parseXMLDocument(String uri, InputStream is,
                                     boolean validate)
            throws JasperException {
        return  parseXMLDocument(uri, new InputSource(is), validate);
    }

    // ------------------------------------------------------ Protected Methods

    /**
     * Create and return a TreeNode that corresponds to the specified Node,
     * including processing all of the attributes and children nodes.
     *
     * @param parent The parent TreeNode (if any) for the new TreeNode
     * @param node The XML document Node to be converted
     */
    protected TreeNode convert(TreeNode parent, Node node) {

        // Construct a new TreeNode for this node
        TreeNode treeNode = new TreeNode(node.getNodeName(), parent);

        // Convert all attributes of this node
        NamedNodeMap attributes = node.getAttributes();
        if (attributes != null) {
            int n = attributes.getLength();
            for (int i = 0; i < n; i++) {
                Node attribute = attributes.item(i);
                treeNode.addAttribute(attribute.getNodeName(),
                                      attribute.getNodeValue());
            }
        }

        // Create and attach all children of this node
        NodeList children = node.getChildNodes();
        if (children != null) {
            int n = children.getLength();
            for (int i = 0; i < n; i++) {
                Node child = children.item(i);
                if (child instanceof Comment)
                    continue;
                if (child instanceof Text) {
                    String body = ((Text) child).getData();
                    if (body != null) {
                        body = body.trim();
                        if (body.length() > 0)
                            treeNode.setBody(body);
                    }
                } else {
                    TreeNode treeChild = convert(treeNode, child);
                }
            }
        }

        // Return the completed TreeNode graph
        return (treeNode);
    }

    // -------------------------------------------------------- Private Methods

    /*
     * Gets the compiled schema referenced by the given XML document.
     *
     * @param document The XML document to validate
     *
     * @return The schema against which to validate
     */
    private static Schema getSchema(Document document)
            throws SAXException, JasperException {

        Schema schema = null;
        Element root = document.getDocumentElement();
        NamedNodeMap map = root.getAttributes();
        for (int i=0; map!=null && i<map.getLength(); i++) {
            if (SCHEMA_LOCATION_ATTR.equals(map.item(i).getLocalName())) {
                String schemaLocation = map.item(i).getNodeValue();
                if (Constants.SCHEMA_LOCATION_JSP_20.equals(schemaLocation)) {
                    schema = getSchema(
                            Constants.TAGLIB_SCHEMA_PUBLIC_ID_20);
                    break;
                } else if (Constants.SCHEMA_LOCATION_JSP_21.equals(
                                                schemaLocation)) {
                    schema = getSchema(
                            Constants.TAGLIB_SCHEMA_PUBLIC_ID_21);
                    break;
                } else if (Constants.SCHEMA_LOCATION_WEBAPP_24.equals(
                                                schemaLocation)) {
                    schema = getSchema(
                            Constants.WEBAPP_SCHEMA_PUBLIC_ID_24);
                    break;
                } else if (Constants.SCHEMA_LOCATION_WEBAPP_25.equals(
                                                schemaLocation)) {
                    schema = getSchema(
                            Constants.WEBAPP_SCHEMA_PUBLIC_ID_25);
                    break;
                } else {
                    throw new JasperException(Localizer.getMessage(
                        "jsp.error.parse.unknownTldSchemaLocation",
                        document.getDocumentURI(),
                        map.item(i).getNodeValue()));
                }
            }
        }

        return schema;
    }

    /*
     * Gets the compiled schema for the given schema public id.
     *
     * @param schemaPublicId The public id for which to get the schema
     * (e.g., web-jsptaglibrary_2_0.xsd)
     *
     * @return The compiled schema
     */
    private static Schema getSchema(String schemaPublicId)
            throws SAXException {

        Schema schema = schemaCache.get(schemaPublicId);
        if (schema == null) {
            synchronized (schemaCache) {
                schema = schemaCache.get(schemaPublicId);
                if (schema == null) {
                    SchemaFactory schemaFactory = SchemaFactory.newInstance(
                        XMLConstants.W3C_XML_SCHEMA_NS_URI);
                    schemaFactory.setResourceResolver(
                        new MyLSResourceResolver());
                    schemaFactory.setErrorHandler(new MyErrorHandler());

                    String path = schemaPublicId;
                    if (schemaResourcePrefix != null) {
                        int index = schemaPublicId.lastIndexOf('/');
                        if (index != -1) {
                            path = schemaPublicId.substring(index+1);
                        }
                        path = schemaResourcePrefix + path;
                    }

                    InputStream input = null;
                    if (isSchemaResourcePrefixFileUrl) {
                        try {
                            File f = new File(new URI(path));
                            if (f.exists()) {
                                input = new FileInputStream(f);
                            }
                        } catch (Exception e) {
                            throw new SAXException(e);
                        }
                    } else {
                        input = ParserUtils.class.getResourceAsStream(path);
                    }

                    if (input == null) {
		        throw new SAXException(
                            Localizer.getMessage(
                                "jsp.error.internal.filenotfound",
                                schemaPublicId));
                    }

                    schema = schemaFactory.newSchema(new StreamSource(input));
                    schemaCache.put(schemaPublicId, schema);
                }
            }
        }

        return schema;
    }

}

// ------------------------------------------------------------ Private Classes

class MyEntityResolver implements EntityResolver {

    public InputSource resolveEntity(String publicId, String systemId)
        throws SAXException
    {
        for (int i=0; i<ParserUtils.CACHED_DTD_PUBLIC_IDS.length; i++) {
            String cachedDtdPublicId = ParserUtils.CACHED_DTD_PUBLIC_IDS[i];
            if (cachedDtdPublicId.equals(publicId)) {
                /* PWC 6386258
                String resourcePath = Constants.CACHED_DTD_RESOURCE_PATHS[i];
                */
                // START PWC 6386258
                String resourcePath = ParserUtils.CACHED_DTD_RESOURCE_PATHS[i];
                // END PWC 6386258
                InputStream input = null;
                if (ParserUtils.isDtdResourcePrefixFileUrl) {
                    try {
                        File path = new File(new URI(resourcePath));
                        if (path.exists()) {
                            input = new FileInputStream(path);
                        }
                    } catch(Exception e) {
                        throw new SAXException(e);
                    }
                } else {
                    input = this.getClass().getResourceAsStream(resourcePath);
                }
                if (input == null) {
                    throw new SAXException(
                        Localizer.getMessage("jsp.error.internal.filenotfound",
                                             resourcePath));
                }
                InputSource isrc = new InputSource(input);
                return isrc;
            }
        }

        if (ParserUtils.log.isLoggable(Level.FINE)) {
            ParserUtils.log.fine("Resolve entity failed"  + publicId + " "
                                  + systemId );
        }

        ParserUtils.log.severe(
            Localizer.getMessage("jsp.error.parse.xml.invalidPublicId",
            publicId));

        return null;
    }
}

class MyErrorHandler implements ErrorHandler {
    public void warning(SAXParseException ex)
	throws SAXException
    {
        if (ParserUtils.log.isLoggable(Level.FINE))
            ParserUtils.log.log(Level.FINE, "ParserUtils: warning ", ex );
	// We ignore warnings
    }

    public void error(SAXParseException ex)
	throws SAXException
    {
	throw ex;
    }

    public void fatalError(SAXParseException ex)
	throws SAXException
    {
	throw ex;
    }
}

class MyLSResourceResolver implements LSResourceResolver {

    public LSInput resolveResource(String type,
                                   String namespaceURI,
                                   String publicId,
                                   String systemId,
                                   String baseURI) {

        InputStream is = null;

        String resourceName = systemId;
        int index = systemId.lastIndexOf('/');
        if (index != -1) {
            resourceName = systemId.substring(index+1);
        }
        String resourcePath = (ParserUtils.schemaResourcePrefix != null) ?
            (ParserUtils.schemaResourcePrefix + resourceName) :
            resourceName;

        if (ParserUtils.isSchemaResourcePrefixFileUrl) {
            try {
                File f = new File(new URI(resourcePath));
                if (f.exists()) {
                    is = new FileInputStream(f);
                }
            } catch (Exception e) {

            }
        } else {
            is = this.getClass().getResourceAsStream(resourcePath);
        }

        MyLSInput ls = new MyLSInput();
        ls.setByteStream(is);
        ls.setSystemId(systemId); // See CR 6402288

        return ls;
    }
}

class MyLSInput implements LSInput {

    private Reader charStream;
    private InputStream byteStream;
    private String stringData;
    private String systemId;
    private String publicId;
    private String baseURI;
    private String encoding;
    private boolean certifiedText;

    public Reader getCharacterStream() {
        return charStream;
    }

    public void setCharacterStream(Reader charStream) {
        this.charStream = charStream;
    }

    public InputStream getByteStream() {
        return byteStream;
    }

    public void setByteStream(InputStream byteStream) {
        this.byteStream = byteStream;
    }

    public String getStringData() {
        return stringData;
    }

    public void setStringData(String stringData) {
        this.stringData = stringData;
    }

    public String getSystemId() {
        return systemId;
    }

    public void setSystemId(String systemId) {
        this.systemId = systemId;
    }

    public String getPublicId() {
        return publicId;
    }

    public void setPublicId(String publicId) {
        this.publicId = publicId;
    }

    public String getBaseURI() {
        return baseURI;
    }

    public void setBaseURI(String baseURI) {
        this.baseURI = baseURI;
    }

    public String getEncoding() {
        return encoding;
    }

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public boolean getCertifiedText() {
        return certifiedText;
    }

    public void setCertifiedText(boolean certifiedText) {
        this.certifiedText = certifiedText;
    }
}


/*******************************************************************************
 * 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
 *******************************************************************************/
package org.eclipse.packagedrone.web.controller.binding;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.eclipse.packagedrone.utils.converter.ConverterManager;
import org.eclipse.packagedrone.utils.reflect.TypeResolver;
import org.eclipse.packagedrone.web.ModelAndView;
import org.eclipse.packagedrone.web.controller.binding.Binder.Initializer;
import org.eclipse.packagedrone.web.controller.validator.ValidationResult;
import org.eclipse.packagedrone.web.controller.validator.Validator;

public class BindingManager
{
    public static class Result extends SimpleBindingResult
    {
    }

    public interface Call
    {
        public Object invoke () throws Exception;
    }

    private final ConverterManager converter;

    public BindingManager ()
    {
        this.converter = ConverterManager.create ();
    }

    /**
     * Create a new BindingManager with default binders
     *

     * This call creates a new BindingManager instance and add the following
     * binders:
     *

     *

     *
{@link MapBinder}

     *
{@link BindingManagerBinder}

     *

     *
     * @param data
     *            the initial data for the MapBinder
     * @return
     *         a new binder manager instance
     */
    public static final BindingManager create ( final Map<String, Object> data )
    {
        final BindingManager result = new BindingManager ();

        result.addBinder ( new MapBinder ( data ) );
        result.addBinder ( new BindingManagerBinder () );

        return result;
    }

    protected Binding bindValue ( final BindTarget target, final ConverterManager converter )
    {
        for ( final Binder binder : this.binders )
        {
            final Binding binding = binder.performBind ( target, converter, this );
            if ( binding != null )
            {
                return binding;
            }
        }
        return Binding.simpleBinding ( converter.getDefault ( target.getType () ) );
    }

    private BindTarget createParameterTarget ( final Parameter parameter, final Object[] args, final int argumentIndex, final TypeResolver typeResolver )
    {
        return new ParameterBindTarget ( parameter, args, argumentIndex, typeResolver );
    }

    protected BindTarget createPropertyTarget ( final Object object, final PropertyDescriptor pd )
    {
        return new PropertyBindTarget ( object, pd );
    }

    public Call bind ( final Method method, final Object targetObject )
    {
        Objects.requireNonNull ( method );
        Objects.requireNonNull ( targetObject );

        final TypeResolver typeResolver = new TypeResolver ( targetObject.getClass () );

        final Parameter[] p = method.getParameters ();

        final Binding[] bindings = new Binding[p.length];
        final Object[] args = new Object[p.length];

        for ( int i = 0; i < p.length; i++ )
        {
            final BindTarget target = createParameterTarget ( p[i], args, i, typeResolver );
            final Binding binding = bindValue ( target, this.converter );

            if ( binding != null )
            {
                bindings[i] = binding;
                target.bind ( binding );

                mergeErrors ( binding.getBindingResult (), this.result );
            }
            else
            {
                throw new IllegalStateException ( String.format ( "Unable to bind parameter '%s' (%s)", p[i].getName (), p[i].getType () ) );
            }
        }

        return new Call () {

            @Override
            public Object invoke () throws Exception
            {
                Object result = method.invoke ( targetObject, args );
                for ( final Binding binding : bindings )
                {
                    result = binding.postProcess ( result );
                }
                result = postProcess ( result );
                return result;
            }
        };
    }

    /**
     * Merge all errors of this binding into this result
     *
     * @param binding
     *            the binding to merge
     * @param result
     *            the result to merge into
     */
    private static void mergeErrors ( final BindingResult bindingResult, final BindingResult result )
    {
        if ( bindingResult == null )
        {
            return;
        }

        result.addErrors ( bindingResult.getLocalErrors () );

        for ( final Map.Entry<String, BindingResult> child : bindingResult.getChildren ().entrySet () )
        {
            mergeErrors ( child.getValue (), result.getChildOrAdd ( child.getKey () ) );
        }
    }

    protected Object postProcess ( final Object result )
    {
        if ( result instanceof ModelAndView )
        {
            ( (ModelAndView)result ).put ( BindingResult.ATTRIBUTE_NAME, this.result );
        }
        return result;
    }

    private final Collection<Binder> binders = new LinkedList<> ();

    private Validator validator;

    private final Result result = new Result ();

    /**
     * Add a new binder
     *

     * If the binder has to be initialized then all methods annotated with
     * {@link Initializer} will be called.
     *

     *
     * @param binder
     *            the binder to add
     * @param initializeBinder
     *            whether the binder will be initialized
     */
    public void addBinder ( final Binder binder, final boolean initializeBinder )
    {
        if ( initializeBinder )
        {
            initializeBinder ( binder );
        }
        this.binders.add ( binder );
    }

    /**
     * Add a new binder
     *

     * The new binder will be initialized.
     *

     *
     * @see #addBinder(Binder, boolean)
     * @param binder
     *            the binder to add
     */
    public void addBinder ( final Binder binder )
    {
        addBinder ( binder, true );
    }

    /**
     * Initialize the binder with our current state
     *
     * @param binder
     *            the binder to initialize
     */
    private void initializeBinder ( final Binder binder )
    {
        for ( final Method m : binder.getClass ().getMethods () )
        {
            if ( !m.isAnnotationPresent ( Binder.Initializer.class ) )
            {
                continue;
            }

            final Call call = bind ( m, binder );

            try
            {
                call.invoke ();
            }
            catch ( final Exception e )
            {
                throw new RuntimeException ( String.format ( "Failed to initialze binder: %s # %s", binder, m ), e );
            }
        }
    }

    public void bindProperties ( final Object o ) throws Exception
    {
        if ( o == null )
        {
            return;
        }

        final Class<?> objectClass = o.getClass ();
        final BeanInfo bi = Introspector.getBeanInfo ( objectClass );

        for ( final PropertyDescriptor pd : bi.getPropertyDescriptors () )
        {
            if ( pd.getWriteMethod () != null )
            {
                final BindTarget target = createPropertyTarget ( o, pd );
                final Binding binding = bindValue ( target, this.converter );
                if ( binding != null )
                {
                    target.bind ( binding );
                    this.result.addChild ( pd.getName (), binding.getBindingResult () );
                }
            }
        }

        validate ( o );
    }

    protected void validate ( final Object o )
    {
        if ( this.validator == null )
        {
            return;
        }

        final ValidationResult vr = this.validator.validate ( o );

        for ( final Map.Entry<String, List<BindingError>> entry : vr.getErrors ().entrySet () )
        {
            this.result.addErrors ( entry.getKey (), entry.getValue () );
        }

        this.result.addMarkers ( vr.getMarkers () );
    }

    public BindingResult getResult ()
    {
        return this.result;
    }

    public void setValidator ( final Validator validator )
    {
        this.validator = validator;
    }
}


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

import static org.eclipse.packagedrone.repo.aspect.PropertiesHelper.loadUrl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;

import org.eclipse.packagedrone.repo.ChannelAspectInformation;
import org.eclipse.packagedrone.repo.Version;
import org.eclipse.packagedrone.repo.aspect.group.Group;
import org.eclipse.packagedrone.repo.aspect.group.GroupInformation;
import org.eclipse.packagedrone.utils.profiler.Profile;
import org.eclipse.packagedrone.utils.profiler.Profile.Handle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

public class ChannelAspectProcessor
{
    private final ServiceTracker<ChannelAspectFactory, FactoryEntry> tracker;

    private ServiceTracker<Group, GroupInformation> groupTracker;

    public static class FactoryEntry
    {

        private final ChannelAspectInformation information;

        private final ChannelAspectFactory service;

        public FactoryEntry ( final ChannelAspectInformation info, final ChannelAspectFactory service )
        {
            this.information = info;
            this.service = service;
        }

        public ChannelAspectInformation getInformation ()
        {
            return this.information;
        }

        public ChannelAspectFactory getService ()
        {
            return this.service;
        }

    }

    public ChannelAspectProcessor ( final BundleContext context )
    {

        this.tracker = new ServiceTracker<ChannelAspectFactory, FactoryEntry> ( context, ChannelAspectFactory.class, new ServiceTrackerCustomizer<ChannelAspectFactory, FactoryEntry> () {

            @Override
            public FactoryEntry addingService ( final ServiceReference<ChannelAspectFactory> reference )
            {
                return makeEntry ( context, reference );
            }

            @Override
            public void modifiedService ( final ServiceReference<ChannelAspectFactory> reference, final FactoryEntry service )
            {
            }

            @Override
            public void removedService ( final ServiceReference<ChannelAspectFactory> reference, final FactoryEntry service )
            {
                context.ungetService ( reference ); // makeEntry got the service
            }
        } );
        this.tracker.open ();

        this.groupTracker = new ServiceTracker<Group, GroupInformation> ( context, Group.class, new ServiceTrackerCustomizer<Group, GroupInformation> () {

            @Override
            public GroupInformation addingService ( final ServiceReference<Group> reference )
            {
                final Group group = context.getService ( reference );
                return group.getInformation ();
            }

            @Override
            public void modifiedService ( final ServiceReference<Group> reference, final GroupInformation service )
            {
            }

            @Override
            public void removedService ( final ServiceReference<Group> reference, final GroupInformation service )
            {
                context.ungetService ( reference );
            }
        } );
        this.groupTracker.open ();
    }

    public void close ()
    {
        this.tracker.close ();
        this.groupTracker.close ();
    }

    protected Map<String, ChannelAspectFactory> getAllFactories ()
    {
        final SortedMap<ServiceReference<ChannelAspectFactory>, FactoryEntry> tracked = this.tracker.getTracked ();

        final Map<String, ChannelAspectFactory> result = new HashMap<> ( tracked.size () );

        for ( final Map.Entry<ServiceReference<ChannelAspectFactory>, FactoryEntry> entry : tracked.entrySet () )
        {
            final Object key = entry.getKey ().getProperty ( ChannelAspectFactory.FACTORY_ID );
            if ( ! ( key instanceof String ) )
            {
                continue;
            }
            result.put ( (String)key, entry.getValue ().getService () );
        }

        return result;
    }

    public <T> void process ( final Collection<String> factoryIds, final Function<ChannelAspect, T> getter, final Consumer<T> consumer )
    {
        final Collection<ChannelAspect> aspects = createAspects ( factoryIds );
        for ( final ChannelAspect aspect : aspects )
        {
            try ( Handle handle = Profile.start ( "processAspect|" + aspect.getId () ) )
            {
                final T t = getter.apply ( aspect );
                if ( t != null )
                {
                    consumer.accept ( t );
                }
            }
        }
    }

    public <T> void process ( final Collection<String> factoryIds, final Function<ChannelAspect, T> getter, final BiConsumer<ChannelAspect, T> consumer )
    {
        final Collection<ChannelAspect> aspects = createAspects ( factoryIds );
        for ( final ChannelAspect aspect : aspects )
        {
            try ( Handle handle = Profile.start ( "processAspect|" + aspect.getId () ) )
            {
                final T t = getter.apply ( aspect );
                if ( t != null )
                {
                    consumer.accept ( aspect, t );
                }
            }
        }
    }

    private Collection<ChannelAspect> createAspects ( final Collection<String> factoryIds )
    {
        final List<String> missingAspects = new LinkedList<> ();

        final Map<String, ChannelAspectFactory> factories = getAllFactories ();

        final List<ChannelAspect> result = new ArrayList<ChannelAspect> ( factoryIds.size () );
        for ( final String id : factoryIds )
        {
            final ChannelAspectFactory factory = factories.get ( id );
            if ( factory == null )
            {
                missingAspects.add ( id );
            }
            else
            {
                final ChannelAspect aspect = factory.createAspect ();
                if ( aspect != null )
                {
                    result.add ( aspect );
                }
                else
                {
                    missingAspects.add ( id );
                }
            }
        }

        if ( !missingAspects.isEmpty () )
        {
            throw new IllegalStateException ( String.format ( "Missing aspects: %s", missingAspects ) );
        }

        return result;
    }

    public Map<String, ChannelAspectInformation> getAspectInformations ()
    {
        final Map<String, ChannelAspectInformation> result = new HashMap<> ();

        for ( final FactoryEntry entry : this.tracker.getTracked ().values () )
        {
            final ChannelAspectInformation info = entry.getInformation ();
            result.put ( info.getFactoryId (), info );
        }

        return result;
    }

    /**
     * This actively scans for available aspects and returns their information
     * objects
     *
     * @param context
     *            the context to use
     * @return the result map, never returns null
     */
    public static Map<String, ChannelAspectInformation> scanAspectInformations ( final BundleContext context )
    {
        Collection<ServiceReference<ChannelAspectFactory>> refs;
        try
        {
            refs = context.getServiceReferences ( ChannelAspectFactory.class, null );
        }
        catch ( final InvalidSyntaxException e )
        {
            // this should never happen since we don't specific a filter
            return Collections.emptyMap ();
        }

        if ( refs == null )
        {
            return Collections.emptyMap ();
        }

        final Map<String, ChannelAspectInformation> result = new HashMap<> ( refs.size () );

        for ( final ServiceReference<ChannelAspectFactory> ref : refs )
        {
            final ChannelAspectInformation info = makeInformation ( ref );
            result.put ( info.getFactoryId (), info );
        }

        return result;
    }

    protected static FactoryEntry makeEntry ( final BundleContext context, final ServiceReference<ChannelAspectFactory> ref )
    {
        final ChannelAspectInformation info = makeInformation ( ref );

        if ( info == null )
        {
            return null;
        }

        return new FactoryEntry ( info, context.getService ( ref ) );
    }

    public static ChannelAspectInformation makeInformation ( final ServiceReference<ChannelAspectFactory> ref )
    {
        final String factoryId = getString ( ref, ChannelAspectFactory.FACTORY_ID, getString ( ref, Constants.SERVICE_PID, null ) );
        if ( factoryId == null )
        {
            return null;
        }

        final String label = getString ( ref, ChannelAspectFactory.NAME, null );

        final String descUrl = getString ( ref, ChannelAspectFactory.DESCRIPTION_FILE, null );
        final String description;
        if ( descUrl != null )
        {
            description = loadUrl ( ref.getBundle (), descUrl );
        }
        else
        {
            description = getString ( ref, ChannelAspectFactory.DESCRIPTION, getString ( ref, Constants.SERVICE_DESCRIPTION, null ) );
        }

        final String groupId = getString ( ref, ChannelAspectFactory.GROUP_ID, null );
        final Version version = Version.valueOf ( getString ( ref, ChannelAspectFactory.VERSION, null ) );

        final SortedSet<String> requires = makeRequires ( ref );

        return new ChannelAspectInformation ( factoryId, label, description, groupId, requires, version );
    }

    private static SortedSet<String> makeRequires ( final ServiceReference<ChannelAspectFactory> ref )
    {
        final Object val = ref.getProperty ( ChannelAspectFactory.REQUIRES );

        if ( val instanceof String[] )
        {
            return new TreeSet<> ( Arrays.asList ( (String[])val ) );
        }
        if ( val instanceof String )
        {
            final String s = (String)val;
            if ( s.isEmpty () )
            {
                return null;
            }
            return new TreeSet<> ( Arrays.asList ( s.split ( "[\\p{Space},]+" ) ) );
        }
        return null;
    }

    public List<ChannelAspectInformation> resolve ( final Collection<String> aspects )
    {
        final Map<String, ChannelAspectInformation> infos = getAspectInformations ();

        final List<ChannelAspectInformation> result = new ArrayList<ChannelAspectInformation> ( aspects.size () );

        for ( final String aspect : aspects )
        {
            final ChannelAspectInformation ai = infos.get ( aspect );
            if ( ai == null )
            {
                result.add ( ChannelAspectInformation.unresolved ( aspect ) );
            }
            else
            {
                result.add ( ai );
            }
        }

        return result;
    }

    private static String getString ( final ServiceReference<ChannelAspectFactory> ref, final String name, final String defaultValue )
    {
        final Object v = ref.getProperty ( name );
        if ( v == null )
        {
            return defaultValue;
        }
        return v.toString ();
    }

    public Collection<GroupInformation> getGroups ()
    {
        return new ArrayList<> ( this.groupTracker.getTracked ().values () );
    }

}


/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2011 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;

/**
 * Contains static utilities for generating SMAP data based on the
 * current version of Jasper.
 *
 * @author Jayson Falkner
 * @author Shawn Bayern
 * @author Robert Field (inner SDEInstaller class)
 * @author Mark Roth
 * @author Kin-man Chung
 */
public class SmapUtil {

    private JspCompilationContext ctxt;
    private List<ClassInfo> classInfos;

    //*********************************************************************
    // Constants

    public static final String SMAP_ENCODING = "UTF-8";

    //*********************************************************************
    // Public entry points

    SmapUtil(JspCompilationContext ctxt) {
        this.ctxt = ctxt;
    }

    /**
     * Generates an appropriate SMAP representing the current compilation
     * context.  (JSR-045.)
     *
     * @param ctxt Current compilation context
     * @param pageNodes The current JSP page
     * @return a SMAP for the page
     */
    public void generateSmap(Node.Nodes pageNodes)
        throws IOException {

        classInfos = new ArrayList<ClassInfo>();
        String className = ctxt.getFullClassName();

        // Instantiate a SmapStratum for the main JSP page.
        SmapStratum s = new SmapStratum("JSP");
        classInfos.add(new ClassInfo(className, s));

        // Map out Node.Nodes and putting LineInfo into SmapStratum
        evaluateNodes(pageNodes, s, ctxt.getOptions().getMappedFile());

        String classFileName = ctxt.getClassFileName();
        for (ClassInfo entry: classInfos) {
            // Get SmapStratum
            s = entry.getSmapStratum();
            s.optimizeLineSection();

            // Set up our SMAP generator
            SmapGenerator g = new SmapGenerator();
            g.setOutputFileName(unqualify(ctxt.getServletJavaFileName()));
            g.addStratum(s, true);

            String name = entry.getClassName();  // class name
            // Compute the class name and output file name for inner classes
            if (!className.equals(name)) {
                classFileName = ctxt.getOutputDir() +
                    name.substring(name.lastIndexOf('.')+1) + ".class";
            }
            entry.setClassFileName(classFileName);
            entry.setSmap(g.getString());

            if (ctxt.getOptions().isSmapDumped()) {
                File outSmap = new File(classFileName + ".smap");
                PrintWriter so =
                    new PrintWriter(
                        new OutputStreamWriter(
                            new FileOutputStream(outSmap),
                            SMAP_ENCODING));
                so.print(g.getString());
                so.close();
            }
        }
    }

    public void installSmap() throws IOException {

        for (ClassInfo ci: classInfos) {
            String className = ci.getClassName();
            byte[] classfile = ctxt.getRuntimeContext().getBytecode(className);
            if (classfile == null) {
                SDEInstaller.install(new File(ci.getClassFileName()),
                             ci.getSmap().getBytes(Charset.defaultCharset()));
            }
            else {
                classfile = SDEInstaller.install(classfile,
                             ci.getSmap().getBytes(Charset.defaultCharset()));
                ctxt.getRuntimeContext().setBytecode(className, classfile);
            }
        }
    }

    //*********************************************************************
    // Private utilities

    /**
     * Returns an unqualified version of the given file path.
     */
    private static String unqualify(String path) {
        path = path.replace('\\', '/');
        return path.substring(path.lastIndexOf('/') + 1);
    }

    //*********************************************************************
    // Installation logic (from Robert Field, JSR-045 spec lead)
    private static class SDEInstaller {

        static final String nameSDE = "SourceDebugExtension";

        byte[] orig;
        byte[] sdeAttr;
        byte[] gen;

        int origPos = 0;
        int genPos = 0;

        int sdeIndex;

        public static void main(String[] args) throws IOException {
            if (args.length == 2) {
                install(new File(args[0]), new File(args[1]));
            } else if (args.length == 3) {
                install(
                    new File(args[0]),
                    new File(args[1]),
                    new File(args[2]));
            } else {
                System.err.println(
                    "Usage: <command> <input class file> "
                        + "<attribute file> <output class file name>\n"
                        + "<command> <input/output class file> <attribute file>");
            }
        }

        static void install(File inClassFile, File attrFile, File outClassFile)
            throws IOException {
            new SDEInstaller(inClassFile, attrFile, outClassFile);
        }

        static void install(File inOutClassFile, File attrFile)
            throws IOException {
            File tmpFile = new File(inOutClassFile.getPath() + "tmp");
            new SDEInstaller(inOutClassFile, attrFile, tmpFile);
            if (!inOutClassFile.delete()) {
                throw new IOException("inOutClassFile.delete() failed");
            }
            if (!tmpFile.renameTo(inOutClassFile)) {
                throw new IOException("tmpFile.renameTo(inOutClassFile) failed");
            }
        }

        static void install(File classFile, byte[] smap) throws IOException {
            File tmpFile = new File(classFile.getPath() + "tmp");
            new SDEInstaller(classFile, smap, tmpFile);
            if (!classFile.delete()) {
                throw new IOException("classFile.delete() failed");
            }
            if (!tmpFile.renameTo(classFile)) {
                throw new IOException("tmpFile.renameTo(classFile) failed");
            }
        }

        static byte[] install(byte[] classfile, byte[] smap)
                throws IOException {
            SDEInstaller installer = new SDEInstaller(classfile, smap);
            byte[] tmp = new byte[installer.genPos];
            System.arraycopy(installer.gen, 0, tmp, 0, installer.genPos);
            return tmp;
        }

        SDEInstaller(byte[] classfile, byte[] sdeAttr)
		throws IOException {
            orig = classfile;
            this.sdeAttr = sdeAttr;
            gen = new byte[orig.length + sdeAttr.length + 100];
            addSDE();
        }

        SDEInstaller(File inClassFile, byte[] sdeAttr, File outClassFile)
            throws IOException {
            if (!inClassFile.exists()) {
                throw new FileNotFoundException("no such file: " + inClassFile);
            }

            this.sdeAttr = sdeAttr;
            // get the bytes
            orig = readWhole(inClassFile);
            gen = new byte[orig.length + sdeAttr.length + 100];

            // do it
            addSDE();

            // write result
            FileOutputStream outStream = new FileOutputStream(outClassFile);
            outStream.write(gen, 0, genPos);
            outStream.close();
        }

        SDEInstaller(File inClassFile, File attrFile, File outClassFile)
            throws IOException {
            this(inClassFile, readWhole(attrFile), outClassFile);
        }

        static byte[] readWhole(File input) throws IOException {
            FileInputStream inStream = new FileInputStream(input);
            int len = (int)input.length();
            byte[] bytes = new byte[len];
            if (inStream.read(bytes, 0, len) != len) {
                throw new IOException("expected size: " + len);
            }
            inStream.close();
            return bytes;
        }

        void addSDE() throws UnsupportedEncodingException, IOException {
            int i;
            copy(4 + 2 + 2); // magic min/maj version
            int constantPoolCountPos = genPos;
            int constantPoolCount = readU2();
            writeU2(constantPoolCount);

            // copy old constant pool return index of SDE symbol, if found
            sdeIndex = copyConstantPool(constantPoolCount);
            if (sdeIndex < 0) {
                // if "SourceDebugExtension" symbol not there add it
                writeUtf8ForSDE();

                // increment the countantPoolCount
                sdeIndex = constantPoolCount;
                ++constantPoolCount;
                randomAccessWriteU2(constantPoolCountPos, constantPoolCount);
            }
            copy(2 + 2 + 2); // access, this, super
            int interfaceCount = readU2();
            writeU2(interfaceCount);
            copy(interfaceCount * 2);
            copyMembers(); // fields
            copyMembers(); // methods
            int attrCountPos = genPos;
            int attrCount = readU2();
            writeU2(attrCount);
            // copy the class attributes, return true if SDE attr found (not copied)
            if (!copyAttrs(attrCount)) {
                // we will be adding SDE and it isn't already counted
                ++attrCount;
                randomAccessWriteU2(attrCountPos, attrCount);
            }
            writeAttrForSDE(sdeIndex);
        }

        void copyMembers() {
            int count = readU2();
            writeU2(count);
            for (int i = 0; i < count; ++i) {
                copy(6); // access, name, descriptor
                int attrCount = readU2();
                writeU2(attrCount);
                copyAttrs(attrCount);
            }
        }

        boolean copyAttrs(int attrCount) {
            boolean sdeFound = false;
            for (int i = 0; i < attrCount; ++i) {
                int nameIndex = readU2();
                // don't write old SDE
                if (nameIndex == sdeIndex) {
                    sdeFound = true;
                } else {
                    writeU2(nameIndex); // name
                    int len = readU4();
                    writeU4(len);
                    copy(len);
                }
            }
            return sdeFound;
        }

        void writeAttrForSDE(int index) {
            writeU2(index);
            writeU4(sdeAttr.length);
            for (int i = 0; i < sdeAttr.length; ++i) {
                writeU1(sdeAttr[i]);
            }
        }

        void randomAccessWriteU2(int pos, int val) {
            int savePos = genPos;
            genPos = pos;
            writeU2(val);
            genPos = savePos;
        }

        int readU1() {
            return ((int)orig[origPos++]) & 0xFF;
        }

        int readU2() {
            int res = readU1();
            return (res << 8) + readU1();
        }

        int readU4() {
            int res = readU2();
            return (res << 16) + readU2();
        }

        void writeU1(int val) {
            gen[genPos++] = (byte)val;
        }

        void writeU2(int val) {
            writeU1(val >> 8);
            writeU1(val & 0xFF);
        }

        void writeU4(int val) {
            writeU2(val >> 16);
            writeU2(val & 0xFFFF);
        }

        void copy(int count) {
            for (int i = 0; i < count; ++i) {
                gen[genPos++] = orig[origPos++];
            }
        }

        byte[] readBytes(int count) {
            byte[] bytes = new byte[count];
            for (int i = 0; i < count; ++i) {
                bytes[i] = orig[origPos++];
            }
            return bytes;
        }

        void writeBytes(byte[] bytes) {
            for (int i = 0; i < bytes.length; ++i) {
                gen[genPos++] = bytes[i];
            }
        }

        int copyConstantPool(int constantPoolCount)
            throws UnsupportedEncodingException, IOException {
            int sdeIndex = -1;
            // copy const pool index zero not in class file
            for (int i = 1; i < constantPoolCount; ++i) {
                int tag = readU1();
                writeU1(tag);
                switch (tag) {
                    case 7 : // Class
                    case 8 : // String
                        copy(2);
                        break;
                    case 9 : // Field
                    case 10 : // Method
                    case 11 : // InterfaceMethod
                    case 3 : // Integer
                    case 4 : // Float
                    case 12 : // NameAndType
                        copy(4);
                        break;
                    case 5 : // Long
                    case 6 : // Double
                        copy(8);
                        i++;
                        break;
                    case 1 : // Utf8
                        int len = readU2();
                        writeU2(len);
                        byte[] utf8 = readBytes(len);
                        String str = new String(utf8, "UTF-8");
                        if (str.equals(nameSDE)) {
                            sdeIndex = i;
                        }
                        writeBytes(utf8);
                        break;
                    default :
                        throw new IOException("unexpected tag: " + tag);
                }
            }
            return sdeIndex;
        }

        void writeUtf8ForSDE() {
            int len = nameSDE.length();
            writeU1(1); // Utf8 tag
            writeU2(len);
            for (int i = 0; i < len; ++i) {
                writeU1(nameSDE.charAt(i));
            }
        }
    }

    private void evaluateNodes(
        Node.Nodes nodes,
        SmapStratum s,
        boolean breakAtLF) {
        try {
            nodes.visit(new SmapGenVisitor(s, breakAtLF, classInfos));
        } catch (JasperException ex) {
        }
    }

    static class SmapGenVisitor extends Node.Visitor {

        private SmapStratum smapStratum;
        private boolean breakAtLF;
        private List<ClassInfo> classInfos;

        SmapGenVisitor(SmapStratum s, boolean breakAtLF,
                       List<ClassInfo> classInfos) {
            this.smapStratum = s;
            this.breakAtLF = breakAtLF;
            this.classInfos = classInfos;
        }

        public void visitBody(Node n) throws JasperException {
            SmapStratum smapStratumSave = smapStratum;
            String innerClass = n.getInnerClassName();
            if (innerClass != null) {
                found: {
                    for (ClassInfo ci: classInfos) {
                        if (innerClass.equals(ci.getClassName())) {
                            smapStratum = ci.getSmapStratum();
                            break found;
                        }
                    }
                    smapStratum = new SmapStratum("JSP");
                    classInfos.add(new ClassInfo(innerClass, smapStratum));
                }
            }
            super.visitBody(n);
            smapStratum = smapStratumSave;
        }

        public void visit(Node.Declaration n) throws JasperException {
            doSmapText(n);
        }

        public void visit(Node.Expression n) throws JasperException {
            doSmapText(n);
        }

        public void visit(Node.Scriptlet n) throws JasperException {
            doSmapText(n);
        }

        public void visit(Node.IncludeAction n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.ForwardAction n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.GetProperty n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.SetProperty n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.UseBean n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.PlugIn n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.CustomTag n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.UninterpretedTag n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.JspElement n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.JspText n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.NamedAttribute n) throws JasperException {
            visitBody(n);
        }

        public void visit(Node.JspBody n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.InvokeAction n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.DoBodyAction n) throws JasperException {
            doSmap(n);
            visitBody(n);
        }

        public void visit(Node.ELExpression n) throws JasperException {
            doSmap(n);
        }

        public void visit(Node.TemplateText n) throws JasperException {
            Mark mark = n.getStart();
            if (mark == null) {
                return;
            }

            //Add the file information
            String fileName = mark.getFile();
            smapStratum.addFile(unqualify(fileName), fileName);

            //Add a LineInfo that corresponds to the beginning of this node
            int iInputStartLine = mark.getLineNumber();
            int iOutputStartLine = n.getBeginJavaLine();
            int iOutputLineIncrement = breakAtLF? 1: 0;
            smapStratum.addLineData(iInputStartLine, fileName, 1,
                             iOutputStartLine, iOutputLineIncrement);

            // Output additional mappings in the text
            java.util.ArrayList extraSmap = n.getExtraSmap();

            if (extraSmap != null) {
                for (int i = 0; i < extraSmap.size(); i++) {
                    iOutputStartLine += iOutputLineIncrement;
                    smapStratum.addLineData(
                        iInputStartLine+((Integer)extraSmap.get(i)).intValue(),
                        fileName,
                        1,
                        iOutputStartLine,
                        iOutputLineIncrement);
                }
            }
        }

        private void doSmap(
            Node n,
            int inLineCount,
            int outIncrement,
            int skippedLines) {
            Mark mark = n.getStart();
            if (mark == null) {
                return;
            }

            String unqualifiedName = unqualify(mark.getFile());
            smapStratum.addFile(unqualifiedName, mark.getFile());
            smapStratum.addLineData(
                mark.getLineNumber() + skippedLines,
                mark.getFile(),
                inLineCount - skippedLines,
                n.getBeginJavaLine() + skippedLines,
                outIncrement);
        }

        private void doSmap(Node n) {
            doSmap(n, 1, n.getEndJavaLine() - n.getBeginJavaLine(), 0);
        }

        private void doSmapText(Node n) {
            String text = n.getText();
            int index = 0;
            int next = 0;
            int lineCount = 1;
            int skippedLines = 0;
            boolean slashStarSeen = false;
            boolean beginning = true;

            // Count lines inside text, but skipping comment lines at the
            // beginning of the text.
            while ((next = text.indexOf('\n', index)) > -1) {
                if (beginning) {
                    String line = text.substring(index, next).trim();
                    if (!slashStarSeen && line.startsWith("/*")) {
                        slashStarSeen = true;
                    }
                    if (slashStarSeen) {
                        skippedLines++;
                        int endIndex = line.indexOf("*/");
                        if (endIndex >= 0) {
                            // End of /* */ comment
                            slashStarSeen = false;
                            if (endIndex < line.length() - 2) {
                                // Some executable code after comment
                                skippedLines--;
                                beginning = false;
                            }
                        }
                    } else if (line.length() == 0 || line.startsWith("//")) {
                        skippedLines++;
                    } else {
                        beginning = false;
                    }
                }
                lineCount++;
                index = next + 1;
            }

            doSmap(n, lineCount, 1, skippedLines);
        }
    }

    /*
     * A place to keep some temporary info on a class.  More than a class
     * may be produced in a compilation, since it may generate inner classes
     */
    static public class ClassInfo {

        private String className;
        private String classFileName;
        private String smap;
        private SmapStratum smapStratum;

        public ClassInfo(String className, SmapStratum smapStratum) {
            this.className = className;
            this.smapStratum = smapStratum;
        }

        public String getClassName() {
            return className;
        }

        public SmapStratum getSmapStratum() {
            return smapStratum;
        }

        public String getClassFileName() {
            return classFileName;
        }

        public void setClassFileName(String classFileName) {
            this.classFileName = classFileName;
        }

        public String getSmap() {
            return smap;
        }

        public void setSmap(String smap) {
            this.smap = smap;
        }
    }

}


/*
 * 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.util.*;
import java.io.*;
import javax.servlet.ServletContext;

import org.apache.jasper.JasperException;
import org.apache.jasper.xmlparser.ParserUtils;
import org.apache.jasper.xmlparser.TreeNode;
import org.apache.jasper.compiler.tagplugin.TagPlugin;
import org.apache.jasper.compiler.tagplugin.TagPluginContext;

/**
 * Manages tag plugin optimizations.
 * @author Kin-man Chung
 */

public class TagPluginManager {

    private static final String TAG_PLUGINS_XML = "/WEB-INF/tagPlugins.xml";
    private static final String TAG_PLUGINS_ROOT_ELEM = "tag-plugins";

    private boolean initialized = false;
    private HashMap<String, TagPlugin> tagPlugins = null;
    private ServletContext ctxt;
    private PageInfo pageInfo;

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

    public void apply(Node.Nodes page, ErrorDispatcher err, PageInfo pageInfo)
	    throws JasperException {

	init(err);
	if (tagPlugins == null || tagPlugins.size() == 0) {
	    return;
	}

	this.pageInfo = pageInfo;

        page.visit(new Node.Visitor() {
            public void visit(Node.CustomTag n)
                    throws JasperException {
                invokePlugin(n);
                visitBody(n);
            }
        });

    }

    private void init(ErrorDispatcher err) throws JasperException {
	if (initialized)
	    return;

	InputStream is = ctxt.getResourceAsStream(TAG_PLUGINS_XML);
	if (is == null)
	    return;

	TreeNode root = (new ParserUtils()).parseXMLDocument(TAG_PLUGINS_XML,
							     is);
	if (root == null) {
	    return;
	}

	if (!TAG_PLUGINS_ROOT_ELEM.equals(root.getName())) {
	    err.jspError("jsp.error.plugin.wrongRootElement", TAG_PLUGINS_XML,
			 TAG_PLUGINS_ROOT_ELEM);
	}

	tagPlugins = new HashMap<String, TagPlugin>();
	Iterator pluginList = root.findChildren("tag-plugin");
	while (pluginList.hasNext()) {
	    TreeNode pluginNode = (TreeNode) pluginList.next();
            TreeNode tagClassNode = pluginNode.findChild("tag-class");
	    if (tagClassNode == null) {
		// Error
		return;
	    }
	    String tagClass = tagClassNode.getBody().trim();
	    TreeNode pluginClassNode = pluginNode.findChild("plugin-class");
	    if (pluginClassNode == null) {
		// Error
		return;
	    }

	    String pluginClassStr = pluginClassNode.getBody();
	    TagPlugin tagPlugin = null;
	    try {
		Class<? extends TagPlugin> pluginClass =
                    Class.forName(pluginClassStr).asSubclass(TagPlugin.class);
		tagPlugin =  pluginClass.newInstance();
	    } catch (Exception e) {
		throw new JasperException(e);
	    }
	    if (tagPlugin == null) {
		return;
	    }
	    tagPlugins.put(tagClass, tagPlugin);
	}
	initialized = true;
    }

    /**
     * Invoke tag plugin for the given custom tag, if a plugin exists for
     * the custom tag's tag handler.
     *
     * The given custom tag node will be manipulated by the plugin.
     */
    private void invokePlugin(Node.CustomTag n) {
	TagPlugin tagPlugin = tagPlugins.get(n.getTagHandlerClass().getName());
	if (tagPlugin == null) {
	    return;
	}

	TagPluginContext tagPluginContext = new TagPluginContextImpl(n, pageInfo);
	n.setTagPluginContext(tagPluginContext);
	tagPlugin.doTag(tagPluginContext);
    }

    static class TagPluginContextImpl implements TagPluginContext {
	private Node.CustomTag node;
	private Node.Nodes curNodes;
	private PageInfo pageInfo;
	private HashMap<String, Object> pluginAttributes;

	TagPluginContextImpl(Node.CustomTag n, PageInfo pageInfo) {
	    this.node = n;
	    this.pageInfo = pageInfo;
	    curNodes = new Node.Nodes();
	    n.setAtETag(curNodes);
	    curNodes = new Node.Nodes();
	    n.setAtSTag(curNodes);
	    n.setUseTagPlugin(true);
	    pluginAttributes = new HashMap<String, Object>();
	}

	public TagPluginContext getParentContext() {
	    Node parent = node.getParent();
	    if (! (parent instanceof Node.CustomTag)) {
		return null;
	    }
	    return ((Node.CustomTag) parent).getTagPluginContext();
	}

	public void setPluginAttribute(String key, Object value) {
	    pluginAttributes.put(key, value);
	}

	public Object getPluginAttribute(String key) {
	    return pluginAttributes.get(key);
	}

	public boolean isScriptless() {
	    return node.getChildInfo().isScriptless();
	}

	public boolean isConstantAttribute(String attribute) {
	    Node.JspAttribute attr = getNodeAttribute(attribute);
	    if (attr == null)
		return false;
	    return attr.isLiteral();
	}

	public String getConstantAttribute(String attribute) {
	    Node.JspAttribute attr = getNodeAttribute(attribute);
            if (attr == null)
		return null;
	    return attr.getValue();
	}

	public boolean isAttributeSpecified(String attribute) {
	    return getNodeAttribute(attribute) != null;
	}

	public String getTemporaryVariableName() {
	    return JspUtil.nextTemporaryVariableName();
	}

	public void generateImport(String imp) {
	    pageInfo.addImport(imp);
	}

	public void generateDeclaration(String id, String text) {
	    if (pageInfo.isPluginDeclared(id)) {
		return;
	    }
	    curNodes.add(new Node.Declaration(text, node.getStart(), null));
	}

	public void generateJavaSource(String sourceCode) {
	    curNodes.add(new Node.Scriptlet(sourceCode, node.getStart(),
					    null));
	}

	public void generateAttribute(String attributeName) {
	    curNodes.add(new Node.AttributeGenerator(node.getStart(),
						     attributeName,
						     node));
	}

	public void dontUseTagPlugin() {
	    node.setUseTagPlugin(false);
	}

	public void generateBody() {
	    // Since we'll generate the body anyway, this is really a nop,
	    // except for the fact that it lets us put the Java sources the
	    // plugins produce in the correct order (w.r.t the body).
	    curNodes = node.getAtETag();
	}

	private Node.JspAttribute getNodeAttribute(String attribute) {
	    Node.JspAttribute[] attrs = node.getJspAttributes();
	    for (int i=0; attrs != null && i < attrs.length; i++) {
		if (attrs[i].getName().equals(attribute)) {
		    return attrs[i];
		}
	    }
	    return null;
	}
    }
}


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

package org.tukaani.xz;

import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * A basic {@link ArrayCache} implementation.
 *

 * This caches exact array sizes, that is, {@code getByteArray} will return
 * an array whose size is exactly the requested size. A limited number
 * of different array sizes are cached at the same time; least recently used
 * sizes will be dropped from the cache if needed (can happen if several
 * different (de)compression options are used with the same cache).
 *

 * The current implementation uses
 * {@link java.util.LinkedHashMap LinkedHashMap} to map different array sizes
 * to separate array-based data structures which hold
 * {@link java.lang.ref.SoftReference SoftReferences} to the cached arrays.
 * In the common case this should give good performance and fairly low
 * memory usage overhead.
 *

 * A statically allocated global {@code BasicArrayCache} instance is
 * available via {@link #getInstance()} which is a good choice in most
 * situations where caching is wanted.
 *
 * @since 1.7
 */
public class BasicArrayCache extends ArrayCache {
    /**
     * Arrays smaller than this many elements will not be cached.
     */
    private static final int CACHEABLE_SIZE_MIN = 32 << 10;

    /**
     * Number of stacks i.e. how many different array sizes to cache.
     */
    private static final int STACKS_MAX = 32;

    /**
     * Number of arrays of the same type and size to keep in the cache.
     * (ELEMENTS_PER_STACK - 1) is used as a bit mask so ELEMENTS_PER_STACK
     * must be a power of two!
     */
    private static final int ELEMENTS_PER_STACK = 512;

    /**
     * A thread-safe stack-like data structure whose {@code push} method
     * overwrites the oldest element in the stack if the stack is full.
     */
    private static class CyclicStack<T> {
        /**
         * Array that holds the elements in the cyclic stack.
         */
        @SuppressWarnings("unchecked")
        private final T[] elements = (T[])new Object[ELEMENTS_PER_STACK];

        /**
         * Read-write position in the {@code refs} array.
         * The most recently added element is in {@code refs[pos]}.
         * If it is {@code null}, then the stack is empty and all
         * elements in {@code refs} are {@code null}.
         *

         * Note that {@code pop()} always modifies {@code pos}, even if
         * the stack is empty. This means that when the first element is
         * added by {@code push(T)}, it can get added in any position in
         * {@code refs} and the stack will start growing from there.
         */
        private int pos = 0;

        /**
         * Gets the most recently added element from the stack.
         * If the stack is empty, {@code null} is returned.
         */
        public synchronized T pop() {
            T e = elements[pos];
            elements[pos] = null;
            pos = (pos - 1) & (ELEMENTS_PER_STACK - 1);
            return e;
        }

        /**
         * Adds a new element to the stack. If the stack is full, the oldest
         * element is overwritten.
         */
        public synchronized void push(T e) {
            pos = (pos + 1) & (ELEMENTS_PER_STACK - 1);
            elements[pos] = e;
        }
    }

    /**
     * Maps Integer (array size) to stacks of references to arrays. At most
     * STACKS_MAX number of stacks are kept in the map (LRU cache).
     */
    private static class CacheMap<T>
            extends LinkedHashMap<Integer, CyclicStack<Reference<T>>> {
        /**
         * This class won't be serialized but this is needed
         * to silence a compiler warning.
         */
        private static final long serialVersionUID = 1L;

        /**
         * Creates a new CacheMap.
         */
        public CacheMap() {
            // The map may momentarily have at most STACKS_MAX + 1 entries
            // when put(K,V) has inserted a new entry but hasn't called
            // removeEldestEntry yet. Using 2 * STACKS_MAX as the initial
            // (and the final) capacity should give good performance. 0.75 is
            // the default load factor and in this case it guarantees that
            // the map will never need to be rehashed because
            // (STACKS_MAX + 1) / 0.75 < 2 * STACKS_MAX.
            //
            // That last argument is true to get LRU cache behavior together
            // with the overriden removeEldestEntry method.
            super(2 * STACKS_MAX, 0.75f, true);
        }

        /**
         * Returns true if the map is full and the least recently used stack
         * should be removed.
         */
        protected boolean removeEldestEntry(
                Map.Entry<Integer, CyclicStack<Reference<T>>> eldest) {
            return size() > STACKS_MAX;
        }
    }

    /**
     * Helper class for the singleton instance.
     * This is allocated only if {@code getInstance()} is called.
     */
    private static final class LazyHolder {
        static final BasicArrayCache INSTANCE = new BasicArrayCache();
    }

    /**
     * Returns a statically-allocated {@code BasicArrayCache} instance.
     * This is often a good choice when a cache is needed.
     */
    public static BasicArrayCache getInstance() {
        return LazyHolder.INSTANCE;
    }

    /**
     * Stacks for cached byte arrays.
     */
    private final CacheMap<byte[]> byteArrayCache = new CacheMap<byte[]>();

    /**
     * Stacks for cached int arrays.
     */
    private final CacheMap<int[]> intArrayCache = new CacheMap<int[]>();

    /**
     * Gets {@code T[size]} from the given {@code cache}.
     * If no such array is found, {@code null} is returned.
     */
    private static <T> T getArray(CacheMap<T> cache, int size) {
        // putArray doesn't add small arrays to the cache and so it's
        // pointless to look for small arrays here.
        if (size < CACHEABLE_SIZE_MIN)
            return null;

        // Try to find a stack that holds arrays of T[size].
        CyclicStack<Reference<T>> stack;
        synchronized(cache) {
            stack = cache.get(size);
        }

        if (stack == null)
            return null;

        // Try to find a non-cleared Reference from the stack.
        T array;
        do {
            Reference<T> r = stack.pop();
            if (r == null)
                return null;

            array = r.get();
        } while (array == null);

        return array;
    }

    /**
     * Puts the {@code array} of {@code size} elements long into
     * the {@code cache}.
     */
    private static <T> void putArray(CacheMap<T> cache, T array, int size) {
        // Small arrays aren't cached.
        if (size < CACHEABLE_SIZE_MIN)
            return;

        CyclicStack<Reference<T>> stack;

        synchronized(cache) {
            // Get a stack that holds arrays of T[size]. If no such stack
            // exists, allocate a new one. If the cache already had STACKS_MAX
            // number of stacks, the least recently used stack is removed by
            // cache.put (it calls removeEldestEntry).
            stack = cache.get(size);
            if (stack == null) {
                stack = new CyclicStack<Reference<T>>();
                cache.put(size, stack);
            }
        }

        stack.push(new SoftReference<T>(array));
    }

    /**
     * Allocates a new byte array, hopefully reusing an existing
     * array from the cache.
     *
     * @param       size        size of the array to allocate
     *
     * @param       fillWithZeros
     *                          if true, all the elements of the returned
     *                          array will be zero; if false, the contents
     *                          of the returned array is undefined
     */
    public byte[] getByteArray(int size, boolean fillWithZeros) {
        byte[] array = getArray(byteArrayCache, size);

        if (array == null)
            array = new byte[size];
        else if (fillWithZeros)
            Arrays.fill(array, (byte)0x00);

        return array;
    }

    /**
     * Puts the given byte array to the cache. The caller must no longer
     * use the array.
     *

     * Small arrays aren't cached and will be ignored by this method.
     */
    public void putArray(byte[] array) {
        putArray(byteArrayCache, array, array.length);
    }

    /**
     * This is like getByteArray but for int arrays.
     */
    public int[] getIntArray(int size, boolean fillWithZeros) {
        int[] array = getArray(intArrayCache, size);

        if (array == null)
            array = new int[size];
        else if (fillWithZeros)
            Arrays.fill(array, 0);

        return array;
    }

    /**
     * Puts the given int array to the cache. The caller must no longer
     * use the array.
     *

     * Small arrays aren't cached and will be ignored by this method.
     */
    public void putArray(int[] array) {
        putArray(intArrayCache, array, array.length);
    }
}