/*

   Derby - Class org.apache.derbyBuild.SecurityPolicyGenerator

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */

package org.apache.derbyBuild;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;
import javax.xml.parsers.*;
import org.w3c.dom.*;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;

/**
 * <p>
 * This tool generates policy files as well as documentation for the Derby Security Guide.
 * This tool consumes securityPolicies.xml and generates a number of policy
 * files and DITA source files. Those files are written to the generated source tree
 * and to the compiled classes tree. See securityPolicies.dtd for a description
 * of the input XML grammar and its concepts.
 * </p>
 */
public class SecurityPolicyGenerator extends Task
{
    /////////////////////////////////////////////////////////////////////////
    //
    //  CONSTANTS
    //
    /////////////////////////////////////////////////////////////////////////

    /** Descriptions of the security policies (relative to the base directory) */
    private static final String SOURCE_DIR = "java/org.apache.derby.engine/org/apache/derby/security";
    private static final String POLICY_DESCRIPTORS = "securityPolicies.xml";
    private static final String PRODUCT_PROPERTIES = "securityProduct.properties";
    private static final String TEST_PROPERTIES = "securityTests.properties";
    private static final String DOC_EXAMPLE_PROPERTIES = "securityDocExample.properties";

    private static final String PERMISSION = "permission ";
    private static final String STATEMENT_END = ";";
    private static final String PRINCIPAL = "principal";

    private static final String TAB = "  ";
    private static final String COMMENT = "// ";
    private static final String NEWLINE = "\n";

    private static final String DO_NOT_EDIT =
      "<!--\n" +
      "\n" +
      "    DO NOT EDIT THIS FILE! THIS FILE IS GENERATED BY SecurityPolicyGenerator\n" +
      "    FROM POLICY DESCRIPTORS IN securityPolicy.xml.\n" +
      "\n" +
      "-->\n";
  
    /////////////////////////////////////////////////////////////////////////
    //
    //  STATE
    //
    /////////////////////////////////////////////////////////////////////////

    /** Base directory of the user's Derby sandbox */
    private File _baseDirectory;

    private Properties _productProperties;
    private Properties _testProperties;
    private Properties _docExampleProperties;

    /////////////////////////////////////////////////////////////////////////
    //
    //  CONSTRUCTORS
    //
    /////////////////////////////////////////////////////////////////////////

   /**
     * <p>
     * Let Ant conjure us out of thin air.
     * </p>
     */
    public SecurityPolicyGenerator()
    {}
    
    /////////////////////////////////////////////////////////////////////////
    //
    //  Task BEHAVIOR
    //
    /////////////////////////////////////////////////////////////////////////
 
    /** <p>Let Ant set the base directory of the Derby sandbox.</p>*/
    public void setBaseDirectory( String baseDirectory ) { _baseDirectory = new File(baseDirectory);}

    /**
     * <p>
     * Read the policy descriptor file and generate policies for
     * various Derby configurations. Also generate documentation on
     * those policies.
     * </p>
     */
    public  void    execute()
        throws BuildException
    {
        try
        {
            _productProperties = loadProperties(PRODUCT_PROPERTIES);
            _testProperties = loadProperties(TEST_PROPERTIES);
            _docExampleProperties = loadProperties(DOC_EXAMPLE_PROPERTIES);
            
            HashMap<String,Policy> policyMap = parsePolicies();
            
            printPolicies(policyMap);
        }
        catch (Exception e)
        {
            e.printStackTrace();
            throw new BuildException( "Could not generate security policies: " + e.getMessage(), e );
        }
        
    }

    /////////////////////////////////////////////////////////////////////////
    //
    //  LOAD THE DESCRIPTOR FILE AND PARSE IT INTO A LIST OF POLICIES
    //
    /////////////////////////////////////////////////////////////////////////

    private HashMap<String,Policy> parsePolicies() throws Exception
    {
        HashMap<String,Policy> retval = new HashMap<String,Policy>();
        String descriptorFileName = sourceFileName(POLICY_DESCRIPTORS);
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(descriptorFileName);
        Element root = doc.getDocumentElement();    // framing "policies" element
        NodeList policies = root.getElementsByTagName("policy");         

        int policyCount = policies.getLength();        
        for (int idx = 0; idx < policyCount; idx++)
        {
            parsePolicy(retval, (Element) policies.item(idx));
        }

        return retval;
    }

    /** Parse a policy and add it to the evolving map of policies */
    private void parsePolicy(HashMap<String,Policy> allPolicies, Element policyElement)
        throws Exception
    {
        int indentLevel = 0;
        String policyName = getLoneText(policyElement, "name");
        Output output = parseOutput(policyElement);
        Policy policy = new Policy(policyName, output, indentLevel);

        allPolicies.put(policyName, policy);

        // include parent policies
        NodeList includeList = policyElement.getElementsByTagName("include");

        if (includeList != null)
        {
            int includeCount = includeList.getLength();

            for (int idx = 0; idx < includeCount; idx++)
            {
                String parentPolicyName = MessageBuilder.squeezeText((Element) includeList.item(idx));
                Policy parentPolicy = allPolicies.get(parentPolicyName);

                policy.include(parentPolicy);
            }
        }

        // grant permissions to jars
        NodeList jarList = policyElement.getElementsByTagName("jar");

        if (jarList != null)
        {
            int jarCount = jarList.getLength();

            for (int idx = 0; idx < jarCount; idx++)
            {
                Element jarElement = (Element) jarList.item(idx);

                parseJar(policy, jarElement, indentLevel);
            }
        }
    }

    /** Parse the output descriptor */
    private Output parseOutput(Element policyElement)
        throws Exception
    {
        Element outputElement = MessageBuilder.getFirstChild(policyElement, "output");
        if (outputElement == null) { return null; }

        File outputFile = new File
          (_baseDirectory.getAbsolutePath() + "/" + getLoneText(outputElement, "file"));
        
        String propertiesName = getLoneText(outputElement, "properties");
        Properties outputProperties = null;

        switch(propertiesName)
        {
        case PRODUCT_PROPERTIES:
          outputProperties = _productProperties;
          break;

        case TEST_PROPERTIES:
          outputProperties = _testProperties;
          break;

        case DOC_EXAMPLE_PROPERTIES:
          outputProperties = _docExampleProperties;
          break;

        default: throw new Exception("Unknown properties file name: " + propertiesName);
        }

        Doc doc = parseDoc(outputElement);
        
        return new Output(outputFile, outputProperties, doc);
    }

    /** Parse optional documentation directives */
    private Doc parseDoc(Element outputElement)
        throws Exception
    {
        Element docElement = MessageBuilder.getFirstChild(outputElement, "doc");
        if (docElement == null) { return null; }

        String title =
          MessageBuilder.squeezeText(MessageBuilder.getFirstChild(docElement, "title"));
        String shortDesc =
          MessageBuilder.squeezeText(MessageBuilder.getFirstChild(docElement, "shortDesc"));
        String majorIndexTerm =
          MessageBuilder.squeezeText(MessageBuilder.getFirstChild(docElement, "majorIndexTerm"));
        String minorIndexTerm =
          MessageBuilder.squeezeText(MessageBuilder.getFirstChild(docElement, "minorIndexTerm"));
        String longDesc =
          MessageBuilder.squeezeText(MessageBuilder.getFirstChild(docElement, "longDesc"));

        return new Doc(title, shortDesc, majorIndexTerm, minorIndexTerm, longDesc);
    }

    /** Parse permission grants to a jar file */
    private void parseJar(Policy policy, Element jarElement, int indentLevel)
        throws Exception
    {
        String targetName = getLoneText(jarElement, "name");
        GrantTarget grantTarget = policy.findGrantTarget(targetName);

        if (grantTarget == null)
        {
            grantTarget = new GrantTarget(targetName, indentLevel);
            policy.add(grantTarget);
        }

        // process blocks of permissions
        NodeList blockList = jarElement.getElementsByTagName("block");

        if (blockList != null)
        {
            int blockCount = blockList.getLength();

            for (int idx = 0; idx < blockCount; idx++)
            {
                Element blockElement = (Element) blockList.item(idx);

                parseBlock(grantTarget, blockElement, indentLevel + 1);
            }
        }
    }

    /** Parse a block of permissions */
    private void parseBlock(GrantTarget grantTarget, Element blockElement, int indentLevel)
        throws Exception
    {
        Element commentElement = MessageBuilder.getFirstChild(blockElement, "comment");
        String comment = (commentElement == null) ? null : MessageBuilder.squeezeText(commentElement);
        PermissionBlock block = new PermissionBlock(comment, indentLevel);

        grantTarget.add(block);

        // first process deleted permissions
        NodeList deletionList = blockElement.getElementsByTagName("d");

        if (deletionList != null)
        {
            int deletionCount = deletionList.getLength();

            for (int idx = 0; idx < deletionCount; idx++)
            {
                Element deletionElement = (Element) deletionList.item(idx);
                String deletedPermission = parsePermission(deletionElement);

                grantTarget.deletePermission(deletedPermission);
            }
        }

        // now process permission adds
        NodeList addList = blockElement.getElementsByTagName("a");

        if (addList != null)
        {
            int addCount = addList.getLength();

            for (int idx = 0; idx < addCount; idx++)
            {
                Element addElement = (Element) addList.item(idx);
                String addPermission = parsePermission(addElement);

                block.add(addPermission);
            }
        }
    }

    /** Get the permission from a permission node */
    private String parsePermission(Element permissionNode) throws Exception
    {
        String text = MessageBuilder.squeezeText(permissionNode);

        return PERMISSION + text + STATEMENT_END;
    }

    /////////////////////////////////////////////////////////////////////////
    //
    //  PRINT THE POLICIES
    //
    /////////////////////////////////////////////////////////////////////////

    private void printPolicies(HashMap<String,Policy> policyMap) throws Exception
    {
        for (Policy policy : policyMap.values())
        {
            // only print policies which have output instructions
            Output output = policy.output;
            if (output == null) { continue; }

            File outputFile = output.file;
            outputFile.getParentFile().mkdirs();

            try (PrintWriter writer = new PrintWriter(outputFile))
            {
                policy.print(writer);
                writer.flush();
            }
        }
    }
  
    /////////////////////////////////////////////////////////////////////////
    //
    //  GENERALLY USEFUL MINIONS
    //
    /////////////////////////////////////////////////////////////////////////

    /** Return the single text child element of the parent element */
    private String getLoneText(Element parentElement, String childName)
      throws Exception
    {
        Element firstElement = MessageBuilder.getFirstChild(parentElement, childName);
        Node    textChild = firstElement.getFirstChild();

        if (textChild == null) { return ""; }
        else { return textChild.getNodeValue(); }
    }

    /** Load a properties file */
    private Properties loadProperties(String shortFileName) throws IOException
    {
        String fullFileName = sourceFileName(shortFileName);
        FileInputStream is = new FileInputStream(fullFileName);
        Properties retval = new Properties();

        retval.load(is);

        return retval;
    }

    /** Turn a short source file name into an absolute file name */
    private String sourceFileName(String shortFileName)
    {
        return _baseDirectory.getAbsolutePath() + "/" + SOURCE_DIR + "/" + shortFileName;
    }
                                                                 
    /**
     * <p>
     * Print a comment.
     * </p>
     */
    private static void printComment(PrintWriter writer, String comment, int indentLevel)
    {
        String tabAndComment = tab(indentLevel) + COMMENT;

        // put comment markers at the beginning of every line
        String prettyComment = tabAndComment + comment.replace(NEWLINE, NEWLINE + tabAndComment);

        writer.println(prettyComment);
    }

    /**
     * <p>
     * Construct a tab level.
     * </p>
     */
    private static String tab(int indentLevel)
    {
        StringBuilder buffer = new StringBuilder();
        for (int idx = 0; idx < indentLevel; idx++) { buffer.append(TAB); }
        return buffer.toString();
    }

    /**
     * <p>
     * Echo a message to the console.
     * </p>
     */
    private void    echo( String text )
    {
        log( text, Project.MSG_WARN );
    }

    /////////////////////////////////////////////////////////////////////////
    //
    //  NESTED CLASSES
    //
    /////////////////////////////////////////////////////////////////////////

    /** A block of permissions */
    public static final class PermissionBlock extends ArrayList<String>
    {
        public final String comment;
        public final int indentLevel;

        public PermissionBlock(String comment, int indentLevel)
        {
          this.comment = comment;
          this.indentLevel = indentLevel;
        }

        // print the permission block
        public void print(PrintWriter writer, boolean forDocumentation) throws IOException
        {
            String tab = tab(indentLevel);
            if (comment != null) { printComment(writer, comment, indentLevel); }

            for (String permission : this)
            {
                // If this is for documentation, then replace angle brackets
                // with xml entities.
                if (forDocumentation)
                {
                    permission = permission
                      .replace("<", "&lt;")
                      .replace(">", "&gt;")
                      ;
                }
                
                writer.print(tab);
                writer.println(permission);
            }
            writer.println();
        }

        public PermissionBlock cloneMe()
        {
            PermissionBlock clone = new PermissionBlock(comment, indentLevel);
            clone.addAll(this);

            return clone;
        }
    }

    /** A target (usually a jar file) which is granted permissions */
    public static final class GrantTarget extends ArrayList<PermissionBlock>
    {
        public final String logicalName;
        public final int indentLevel;

        public GrantTarget(String logicalName, int indentLevel)
        {
          this.logicalName = logicalName;
          this.indentLevel = indentLevel;
        }

        /** Delete a permission from all blocks */
        public void deletePermission(String permission)
        {
            for (PermissionBlock block : this)
            {
                block.remove(permission);
            }
        }
        
        // print the granted permissions
        public void print
          (
           PrintWriter writer,
           Properties outputProperties,
           boolean forDocumentation
           ) throws IOException
        {
            String tab = tab(indentLevel);
            String actualName = outputProperties.getProperty(logicalName, logicalName);
            boolean isPrincipal = actualName.startsWith(PRINCIPAL);

            writer.print(tab);
            writer.print("grant");
            if (actualName.length() > 0)
            {
                writer.print(" ");
                if (isPrincipal)
                {
                    writer.println(actualName);
                }
                else
                {
                    writer.print("codeBase \"");
                    writer.print(actualName);
                    writer.println("\"");
                }
            }
            else { writer.println(""); }

            writer.print(tab);
            writer.println("{");

            for (PermissionBlock block : this)
            {
                block.print(writer, forDocumentation);
            }

            writer.print(tab);
            writer.println("};");
            writer.println();
        }

        public GrantTarget cloneMe()
        {
            GrantTarget clone = new GrantTarget(logicalName, indentLevel);

            for (PermissionBlock block : this)
            {
                clone.add(block.cloneMe());
            }

            return clone;
        }
    }

    /** A descriptor for documentation directives */
    public static final class Doc
    {
        public final String title;
        public final String shortDesc;
        public final String majorIndexTerm;
        public final String minorIndexTerm;
        public final String longDesc;

        public Doc
          (
           String title,
           String shortDesc,
           String majorIndexTerm,
           String minorIndexTerm,
           String longDesc
           )
        {
            this.title = title;
            this.shortDesc = shortDesc;
            this.majorIndexTerm = majorIndexTerm;
            this.minorIndexTerm = minorIndexTerm;
            this.longDesc = longDesc;
        }
      
        // print the dita header
        public void printDitaHeader(MessageBuilder.XMLWriter writer, String referenceID) throws IOException
        {
            writer.println(MessageBuilder.REF_GUIDE_BOILERPLATE);
            writer.println("<!--");
            writer.println(MessageBuilder.APACHE_LICENSE);
            writer.println("-->");
            writer.println(DO_NOT_EDIT);

            writer.beginTag("reference", "id=\"" + referenceID + "\" xml:lang=\"en-us\"");
            {
                writer.writeTextElement("title", title);
                writer.writeTextElement("shortdesc", shortDesc);

                writer.beginTag("prolog");
                {
                    writer.beginTag("metadata");
                    {
                        writer.beginTag("keywords");
                        {
                            writer.beginTag("indexterm");
                            {
                                writer.indent();
                                writer.println(majorIndexTerm);
                                writer.writeTextElement("indexterm", minorIndexTerm);
                            }
                            writer.endTag();
                        }
                        writer.endTag();
                    }
                    writer.endTag();
                }
                writer.endTag();

                writer.beginTag("refbody");
                {
                    writer.beginTag("section");
                    {
                        writer.writeTextElement("p", longDesc);
                      
                        writer.beginTag("codeblock");
                        {
                            // caller fills in the codeblock, the bulk of the policy file
                        }
                        // terminated by printDitaFooter()
                    }
                    // terminated by printDitaFooter()
                }
                // terminated by printDitaFooter()
            }
            // terminated by printDitaFooter()
        }
      
        // print the dita header
        public void printDitaFooter(MessageBuilder.XMLWriter writer) throws IOException
        {
            writer.endTag();  // end codeblock
            writer.endTag();  // end section
            writer.endTag();  // end refbody
            writer.endTag();  // end reference
        }
    }
  
    /** A descriptor for where to write the policy file */
    public static final class Output
    {
        public final File file;
        public final Properties properties;
        public final Doc doc;

        public Output(File file, Properties properties, Doc doc)
        {
            this.file = file;
            this.properties = properties;
            this.doc = doc;
        }

        /**
         * The DITA reference id is the file stub.
         */
        public String ditaReferenceID()
        {
            String shortName = file.getName();
            int dotIdx = shortName.lastIndexOf(".");

            return shortName.substring(0, dotIdx);
        }
    }
  
    /** A policy, consisting of GrantTargets */
    public static final class Policy extends ArrayList<GrantTarget>
    {
        public final String name;
        public final Output output;
        public final int indentLevel;

        public Policy(String name, Output output, int indentLevel)
        {
          this.name = name;
          this.output = output;
          this.indentLevel = indentLevel;
        }

        /** Include a parent policy */
        public void include(Policy parent)
        {
            for (GrantTarget source : parent)
            {
                add(source.cloneMe());
            }
        }

        /**
         * Add a GrantTarget, merging into an existing target of the
         * same name if it exists.
         */
        @Override
        public boolean add(GrantTarget source)
        {
            GrantTarget destination = findGrantTarget(source.logicalName);

            if (destination == null) { return super.add(source); }
            else
            {
                return destination.addAll(source);
            }
         }

        /** Find a GrantTarget */
        public GrantTarget findGrantTarget(String targetName)
        {
            for (GrantTarget candidate : this)
            {
                if (targetName.equals(candidate.logicalName)) { return candidate; }
            }

            return null;
        }
        
        // print the policy file
        public void print(PrintWriter writer) throws IOException
        {
            String tab = tab(indentLevel);
            MessageBuilder.XMLWriter xmlWriter = null;
            Doc doc = null;
            boolean forDocumentation = false;

            if (output != null)
            {
                doc = output.doc;
                if (doc != null)
                {
                    forDocumentation = true;
                    
                    // Frame the Security Guide page with boilerplate
                    xmlWriter = new MessageBuilder.XMLWriter(writer);

                    doc.printDitaHeader(xmlWriter, output.ditaReferenceID());
                }
            }

            //
            // If we are printing a Security Guide page, then don't redundantly print
            // the Apache license, which printDitaHeader() already wrote.
            //
            if (!forDocumentation)
            {
                printComment(writer, MessageBuilder.APACHE_LICENSE, indentLevel);
                writer.println();
            }

            Properties outputProperties = (output == null) ? new Properties() : output.properties;

            for (GrantTarget grantTarget : this)
            {
                grantTarget.print(writer, outputProperties, forDocumentation);
            }
            writer.println();

            if (forDocumentation)
            {
                doc.printDitaFooter(xmlWriter);
                xmlWriter.flush();
            }
        }
    }

}

