MessageListXmlReader.java

/* *******************************************************************
 * Copyright (c) 2003 Contributors.
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Wes Isberg     initial implementation
 * ******************************************************************/

package org.aspectj.testing.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.digester3.Digester;
import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.util.LangUtil;
import org.xml.sax.SAXException;

/**
 * Read a list of messages in xml form.
 * Input files should comply with DOCTYPE
 */
public class MessageListXmlReader {
    private static final String INLINE_DOCTYPE;
    static {
        final String EOL = LangUtil.EOL;
        final StringBuilder r = new StringBuilder();

        r.append("<!DOCTYPE ");
        r.append(MessageList.XMLNAME);
        r.append(" [");
        r.append(EOL + "   <!ELEMENT " + MessageList.XMLNAME
                    + " (" + SoftMessage.XMLNAME + "+) >");
        String name = SoftMessage.XMLNAME;
        r.append(EOL + "   <!ELEMENT " + name + " (" + SoftSourceLocation.XMLNAME + ")*>");
        r.append(EOL + "   <!ATTLIST " + name + " kind CDATA #REQUIRED >");
        r.append(EOL + "   <!ATTLIST " + name + " message CDATA #IMPLIED >");
        name = SoftSourceLocation.XMLNAME;
        r.append(EOL + "   <!ELEMENT " + name + " (#PCDATA) >");
        r.append(EOL + "   <!ATTLIST " + name + " line CDATA #REQUIRED >");
        r.append(EOL + "   <!ATTLIST " + name + " endLine CDATA #IMPLIED >");
        r.append(EOL + "   <!ATTLIST " + name + " column CDATA #IMPLIED >");
        r.append(EOL + "   <!ATTLIST " + name + " sourceFile CDATA #IMPLIED >");

        r.append(EOL + "] >");
        INLINE_DOCTYPE = r.toString();
    }

    private static final String[] LOG = new String[] {"info", "debug", "trace" };

    private int logLevel;

    /**
     * Print IMessage[] to the output file as XML.
     * @param output the File to write to - overwritten
     * @param messages the IMessage[] to write
     * @return null if no warnings detected, warnings otherwise
     */
    public String writeMessages(File output, IMessage[] messages) throws IOException {
        LangUtil.throwIaxIfNull(output, "output");
        LangUtil.throwIaxIfFalse(!LangUtil.isEmpty(messages), "no messages");
        PrintWriter writer = new PrintWriter(new FileOutputStream(output));
        XMLWriter printSink = new XMLWriter(writer);
        writer.println("");
        writer.println(INLINE_DOCTYPE);
        writer.println("");
        writer.println("<" + MessageList.XMLNAME + ">");
        SoftMessage.writeXml(printSink, messages);
        writer.println("</" + MessageList.XMLNAME + ">");
        writer.close();
        return null;
    }

    /** @param level 0..2, info..trace */
    public void setLogLevel(int level) {
        if (level < 0) {
            level = 0;
        }
        if (level > 2) {
            level = 2;
        }
        logLevel = level;
    }

    /**
     * Read the specifications for a list of IMessage from an XML file.
     * @param file the File must be readable, comply with DOCTYPE.
     * @return IMessage[] read from file
     * @see setLogLevel(int)
     */
    public IMessage[] readMessages(File file) throws IOException, AbortException {
        // setup loggers for digester and beanutils...
        System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); // XXX
        System.setProperty("org.apache.commons.logging.simplelog.defaultlog", LOG[logLevel]); // trace debug XXX

        final Digester digester = new Digester();
        setupDigester(digester);

        MessageListHolder holder = new MessageListHolder();
        digester.push(holder);
        FileInputStream input = new FileInputStream(file);
        try {
            digester.parse(input);
        } catch (SAXException e) {
            MessageUtil.fail("parsing " + file, e);
        } finally {
            if (null != input) {
                input.close();
                input = null;
            }
        }
        return (null == holder.list
            ? new IMessage[0]
            : holder.list.getMessages());
    }

    /** set up the mapping between the xml and Java. */
    private void setupDigester(Digester digester) {
        // XXX supply sax parser to ignore white space?
        digester.setValidating(true);

        // element names come from the element components
        final String messageListX = MessageList.XMLNAME;
        final String messageX = messageListX + "/" + SoftMessage.XMLNAME;
        final String messageSrcLocX = messageX + "/" + SoftSourceLocation.XMLNAME;

        // ---- each sub-element needs to be created
        // handle messages the same at any level
        digester.addObjectCreate(messageListX,         MessageList.class.getName());
        digester.addObjectCreate(messageX,             SoftMessage.class.getName());
        digester.addObjectCreate(messageSrcLocX,       SoftSourceLocation.class.getName());

        // ---- set bean properties for sub-elements created automatically
        // -- some remapped - warnings
        //   - if property exists, map will not be used
        digester.addSetProperties(messageListX);
        digester.addSetProperties(messageX);
        digester.addSetProperties(messageSrcLocX);
        digester.addSetProperties(messageX, "kind", "kindAsString");
        digester.addSetProperties(messageX, "line", "lineAsString");

        // ---- when subelements are created, add to parent
        digester.addSetNext(messageListX,         "addMessage", IMessage.class.getName());
        digester.addSetNext(messageX,             "setSourceLocation", ISourceLocation.class.getName());

        // can set parent, but prefer to have "knows-about" flow down only...
    }

    // ------------------------------------------------------------ testing code

    /**
     * This is only to do compile-time checking for the APIs impliedly
     * used in setupDigester(..).
     * The property setter checks are redundant with tests based on
     * expectedProperties().
     */
//    private static void setupDigesterCompileTimeCheck() {
//        if (true) { throw new Error("never invoked"); }
//
//        MessageListHolder holder = new MessageListHolder();
//        MessageList ml = new MessageList();
//        SoftMessage m = new SoftMessage();
//        SoftSourceLocation sl = new SoftSourceLocation();
//
//        holder.setMessageList(ml);
//        ml.addMessage((IMessage) null);
//        m.setSourceLocation(sl);
//        m.setText((String) null);
//        m.setKindAsString((String) null);
//
//        sl.setFile((String) null);
//        sl.setLine((String) null);
//        sl.setColumn((String) null);
//        sl.setEndLine((String) null);
//
//        // add attribute setters to validate?
//    }

    // inner classes, to make public for bean utilities
    /** a list of messages */
    public static class MessageList {
        public static final String XMLNAME = "message-list";
        private List messages = new ArrayList();
        public void addMessage(IMessage message) {
            messages.add(message);
        }
        IMessage[] getMessages() {
            return (IMessage[]) messages.toArray(new IMessage[0]);
        }
    }

    /** push on digester stack to hold message list */
    public static class MessageListHolder {
        public MessageList list;
        public void setMessageList(MessageList list) {
            this.list = list;
        }
    }

}