XmlConfigurator.java

package com.ziesemer.utils.web.addressFilter;

import java.util.ArrayList;
import java.util.List;

import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import com.ziesemer.utils.web.addressFilter.actions.AllowAction;
import com.ziesemer.utils.web.addressFilter.actions.IAction;
import com.ziesemer.utils.web.addressFilter.actions.RejectAction;
import com.ziesemer.utils.web.addressFilter.actions.RequestAttributeAction;
import com.ziesemer.utils.web.addressFilter.conditions.AnyCondition;
import com.ziesemer.utils.web.addressFilter.conditions.HostNameOnDemandAddressChecker;
import com.ziesemer.utils.web.addressFilter.conditions.HostNameStartupAddressChecker;
import com.ziesemer.utils.web.addressFilter.conditions.IAddressChecker;
import com.ziesemer.utils.web.addressFilter.conditions.ICondition;
import com.ziesemer.utils.web.addressFilter.conditions.IPAddressCheckerEmpty;
import com.ziesemer.utils.web.addressFilter.conditions.IPAddressCheckerFactory;
import com.ziesemer.utils.web.addressFilter.conditions.IPPortCheckerFactory;
import com.ziesemer.utils.web.addressFilter.conditions.IPortChecker;
import com.ziesemer.utils.web.addressFilter.conditions.IStringMatcher;
import com.ziesemer.utils.web.addressFilter.conditions.IsSecureCondition;
import com.ziesemer.utils.web.addressFilter.conditions.LocalAddrCondition;
import com.ziesemer.utils.web.addressFilter.conditions.LocalHostOnDemandAddressChecker;
import com.ziesemer.utils.web.addressFilter.conditions.LocalHostStartupAddressChecker;
import com.ziesemer.utils.web.addressFilter.conditions.LocalPortCondition;
import com.ziesemer.utils.web.addressFilter.conditions.RemoteAddrCondition;
import com.ziesemer.utils.web.addressFilter.conditions.RemotePortCondition;
import com.ziesemer.utils.web.addressFilter.conditions.RequestAttributeCondition;
import com.ziesemer.utils.web.addressFilter.conditions.SchemeCondition;
import com.ziesemer.utils.web.addressFilter.conditions.StringMatcherExact;
import com.ziesemer.utils.web.addressFilter.conditions.StringMatcherRegex;
import com.ziesemer.utils.web.addressFilter.conditions.UserInRoleCondition;
import com.ziesemer.utils.web.addressFilter.conditions.XForwardedForCondition;

/**
 * @author Mark A. Ziesemer
 * 	<a href="http://www.ziesemer.com.">&lt;www.ziesemer.com&gt;</a>
 */
public class XmlConfigurator extends DefaultHandler{
	
	protected static final Logger LOGGER = LoggerFactory.getLogger(XmlConfigurator.class);
	
	protected static final String NS = "http://namespaces.ziesemer.com/utils.web/addressFilter";
	
	protected InputSource source;
	
	protected enum State{
		DEFAULT, UNKNOWN, OTHER,
		ADDRESS_FILTER, ADDRESS_GROUP, DEFAULT_ACTION,
		LOCAL_ADDR, LOCAL_PORT, REMOTE_ADDR, REMOTE_PORT,
			X_FORWARDED_FOR, IS_SECURE, SCHEME, USER_IN_ROLE, REQUEST_ATTR, ACTION,
		IP_ADDRESS, HOST_NAME, LOCALHOST, PORT, PROXY_IDX,
		STRING_MATCH, RESOLUTION,
		EXACT_MATCH, REGEX_MATCH,
		SET_REQUEST_ATTR, REQUEST_ATTR_NAME, REQUEST_ATTR_VALUE;
	}
	
	protected enum Resolution{
		Startup, OnDemand;
	}
	
	protected String filterName;
	protected Locator locator;
	
	protected State state = State.DEFAULT;
	protected List<State> stateStack = new ArrayList<State>();
	protected StringBuilder charBuffer = new StringBuilder();
	
	protected List<AddressGroup> addressGroups;
	protected List<IAction> defaultActions;
	
	protected String addressGroupId;
	protected List<ICondition> conditions = new ArrayList<ICondition>();
	protected List<IAddressChecker> addressCheckers = new ArrayList<IAddressChecker>();
	protected List<IPortChecker> portCheckers = new ArrayList<IPortChecker>();
	protected List<IAction> groupActions = new ArrayList<IAction>();
	protected List<IAction> actions;
	protected int proxyIndex;
	protected Resolution resolution;
	protected IStringMatcher stringMatcher;
	protected String requestAttrName;
	protected String requestAttrValue;
	
	public XmlConfigurator(final InputSource source){
		this.source = source;
	}
	
	@Override
	public void setDocumentLocator(final Locator locator){
		super.setDocumentLocator(locator);
		this.locator = locator;
	}
	
	public void configure(final String filterName, final List<AddressGroup> groups, final List<IAction> defaultActions) throws Exception{
		this.filterName = filterName;
		
		final SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
		final Schema schema = sf.newSchema(new StreamSource(XmlConfigurator.class.getResourceAsStream("AddressFilter.xsd")));
		
		final SAXParserFactory spf = SAXParserFactory.newInstance();
		spf.setNamespaceAware(true);
		spf.setSchema(schema);
		
		this.addressGroups = groups;
		this.defaultActions = defaultActions;
		
		final SAXParser sp = spf.newSAXParser();
		sp.parse(source, this);
	}
	
	@Override
	public void warning(final SAXParseException e) throws SAXException{
		LOGGER.warn("({}) {} {}", filterName, e, getLocatorFormatter(e), e);
		super.warning(e);
	}
	
	@Override
	public void error(final SAXParseException e) throws SAXException{
		LOGGER.error("({}) {} {}", filterName, e, getLocatorFormatter(e), e);
		throw e;
	}
	
	@Override
	public void fatalError(final SAXParseException e) throws SAXException{
		LOGGER.error("({}) {} {}", filterName, e, getLocatorFormatter(e), e);
		throw e;
	}
	
	@Override
	public void startElement(final String uri, final String localName, final String qName, final Attributes attributes)
			throws SAXException{
		
		stateStack.add(state);
		
		State newState = State.UNKNOWN;
		
		if(NS.equals(uri)){
			switch(state){
				case DEFAULT: {
					if("AddressFilter".equals(localName)){
						newState = State.ADDRESS_FILTER;
					}
					break;
				}
				case ADDRESS_FILTER: {
					if("AddressGroup".equals(localName)){
						addressGroupId = attributes.getValue("groupId");
						actions = groupActions;
						newState = State.ADDRESS_GROUP;
					}else if("DefaultAction".equals(localName)){
						actions = defaultActions;
						newState = State.DEFAULT_ACTION;
					}
					break;
				}
				case ADDRESS_GROUP: {
					if("LocalAddr".equals(localName)){
						newState = State.LOCAL_ADDR;
					}else if("LocalPort".equals(localName)){
						newState = State.LOCAL_PORT;
					}else if("RemoteAddr".equals(localName)){
						newState = State.REMOTE_ADDR;
					}else if("RemotePort".equals(localName)){
						newState = State.REMOTE_PORT;
					}else if("X-Forwarded-For".equals(localName)){
						newState = State.X_FORWARDED_FOR;
					}else if("IsSecure".equals(localName)){
						newState = State.IS_SECURE;
					}else if("Scheme".equals(localName)){
						newState = State.SCHEME;
					}else if("UserInRole".equals(localName)){
						newState = State.USER_IN_ROLE;
					}else if("RequestAttribute".equals(localName)){
						newState = State.REQUEST_ATTR;
					}else if("Action".equals(localName)){
						newState = State.ACTION;
					}
					break;
				}
				case LOCAL_ADDR:
				case REMOTE_ADDR: {
					if("IPAddress".equals(localName)){
						newState = State.IP_ADDRESS;
					}else if("HostName".equals(localName)){
						newState = State.HOST_NAME;
					}else if("LocalHost".equals(localName)){
						newState = State.LOCALHOST;
					}
					break;
				}
				case LOCAL_PORT:
				case REMOTE_PORT: {
					if("Port".equals(localName)){
						newState = State.PORT;
					}
					break;
				}
				case X_FORWARDED_FOR: {
					if("ProxyIndex".equals(localName)){
						newState = State.PROXY_IDX;
					}else if("IPAddress".equals(localName)){
						newState = State.IP_ADDRESS;
					}else if("HostName".equals(localName)){
						newState = State.HOST_NAME;
					}else if("LocalHost".equals(localName)){
						newState = State.LOCALHOST;
					}else if("Empty".equals(localName)){
						newState = State.OTHER;
						addressCheckers.add(IPAddressCheckerEmpty.INSTANCE);
					}
					break;
				}
				case REQUEST_ATTR: {
					if("Name".equals(localName)){
						newState = State.REQUEST_ATTR_NAME;
					}else if("Value".equals(localName)){
						newState = State.STRING_MATCH;
					}
					break;
				}
				case DEFAULT_ACTION:
				case ACTION: {
					if("ALLOW".equals(localName)){
						newState = State.OTHER;
						actions.add(AllowAction.INSTANCE);
					}else if("REJECT".equals(localName)){
						newState = State.OTHER;
						actions.add(RejectAction.INSTANCE);
					}else if("SetRequestAttribute".equals(localName)){
						newState = State.SET_REQUEST_ATTR;
					}
					break;
				}
				case HOST_NAME: {
					if("Name".equals(localName)){
						newState = State.STRING_MATCH;
					}else if("Resolution".equals(localName)){
						newState = State.RESOLUTION;
					}
					break;
				}
				case LOCALHOST: {
					if("Resolution".equals(localName)){
						newState = State.RESOLUTION;
					}
					break;
				}
				case STRING_MATCH: {
					if("ExactMatch".equals(localName)){
						newState = State.EXACT_MATCH;
					}else if("RegexMatch".equals(localName)){
						newState = State.REGEX_MATCH;
					}
					break;
				}
				case RESOLUTION: {
					if("Startup".equals(localName)){
						newState = State.OTHER;
						resolution = Resolution.Startup;
					}else if("OnDemand".equals(localName)){
						newState = State.OTHER;
						resolution = Resolution.OnDemand;
					}
					break;
				}
				case SET_REQUEST_ATTR: {
					if("Name".equals(localName)){
						newState = State.REQUEST_ATTR_NAME;
					}else if("Value".equals(localName)){
						newState = State.REQUEST_ATTR_VALUE;
					}
					break;
				}
				default: {
					break;
				}
			}
		}
		
		state = newState;
		
		if(state == State.UNKNOWN){
			LOGGER.warn("({}) Unrecognized element: {} {}",
				filterName, localName, getLocatorFormatter());
		}
		
		charBuffer.setLength(0);
	}
	
	@Override
	public void characters(final char[] ch, final int start, final int length) throws SAXException{
		charBuffer.append(ch, start, length);
	}
	
	protected <T> List<T> copyClear(final List<T> list){
		final ArrayList<T> result = new ArrayList<T>(list);
		// In JDK 1.5, the array will have been created with a capacity of 110% of the specified collection.
		// In JDK 1.6 and 1.7, trimToSize() is technically unnecessary.
		result.trimToSize();
		list.clear();
		return result;
	}
	
	@Override
	public void endElement(final String uri, final String localName, final String qName) throws SAXException{
		try{
			switch(state){
				case ADDRESS_GROUP: {
					addressGroups.add(new AddressGroup(addressGroupId, copyClear(conditions), copyClear(actions)));
					break;
				}
				case DEFAULT_ACTION: {
					defaultActions.addAll(copyClear(groupActions));
					break;
				}
				case LOCAL_ADDR: {
					conditions.add(new LocalAddrCondition(copyClear(addressCheckers)));
					break;
				}
				case LOCAL_PORT: {
					if(portCheckers.size() > 1){
						final List<ICondition> anyList = new ArrayList<ICondition>(portCheckers.size());
						for(final IPortChecker checker : portCheckers){
							anyList.add(new LocalPortCondition(checker));
						}
						conditions.add(new AnyCondition(anyList));
					}else if(portCheckers.size() == 1){
						conditions.add(new LocalPortCondition(portCheckers.get(0)));
					}
					portCheckers.clear();
					break;
				}
				case REMOTE_ADDR: {
					conditions.add(new RemoteAddrCondition(copyClear(addressCheckers)));
					break;
				}
				case REMOTE_PORT: {
					if(portCheckers.size() > 1){
						final List<ICondition> anyList = new ArrayList<ICondition>(portCheckers.size());
						for(final IPortChecker checker : portCheckers){
							anyList.add(new RemotePortCondition(checker));
						}
						conditions.add(new AnyCondition(anyList));
					}else if(portCheckers.size() == 1){
						conditions.add(new RemotePortCondition(portCheckers.get(0)));
					}
					portCheckers.clear();
					break;
				}
				case X_FORWARDED_FOR: {
					conditions.add(new XForwardedForCondition(copyClear(addressCheckers), proxyIndex));
					proxyIndex = 0;
					break;
				}
				case IS_SECURE: {
					conditions.add(new IsSecureCondition("true".equals(charBuffer.toString())));
					break;
				}
				case SCHEME: {
					conditions.add(new SchemeCondition(charBuffer.toString()));
					break;
				}
				case USER_IN_ROLE: {
					conditions.add(new UserInRoleCondition(charBuffer.toString()));
					break;
				}
				case REQUEST_ATTR: {
					conditions.add(new RequestAttributeCondition(requestAttrName, stringMatcher));
					break;
				}
				case IP_ADDRESS: {
					addressCheckers.add(IPAddressCheckerFactory.createChecker(charBuffer.toString()));
					break;
				}
				case HOST_NAME: {
					switch(resolution){
						case Startup: {
							if(!(stringMatcher instanceof StringMatcherExact)){
								throw new Exception("HostName checker with Startup resolution must use ExactMatch.");
							}
							final String hostName = ((StringMatcherExact)stringMatcher).getValue();
							addressCheckers.add(new HostNameStartupAddressChecker(hostName));
							break;
						}
						case OnDemand: {
							addressCheckers.add(new HostNameOnDemandAddressChecker(stringMatcher));
							break;
						}
						default: {
							throw new Exception("Unrecognized resolution type: " + resolution);
						}
					}
					break;
				}
				case LOCALHOST: {
					switch(resolution){
						case Startup: {
							addressCheckers.add(new LocalHostStartupAddressChecker());
							break;
						}
						case OnDemand: {
							addressCheckers.add(new LocalHostOnDemandAddressChecker());
							break;
						}
						default: {
							throw new Exception("Unrecognized resolution type: " + resolution);
						}
					}
					break;
				}
				case PORT: {
					portCheckers.add(IPPortCheckerFactory.createChecker(charBuffer.toString()));
					break;
				}
				case PROXY_IDX: {
					proxyIndex = Integer.parseInt(charBuffer.toString());
					break;
				}
				case EXACT_MATCH: {
					stringMatcher = new StringMatcherExact(charBuffer.toString());
					break;
				}
				case REGEX_MATCH: {
					stringMatcher = new StringMatcherRegex(charBuffer.toString());
					break;
				}
				case SET_REQUEST_ATTR: {
					actions.add(new RequestAttributeAction(requestAttrName, requestAttrValue));
					break;
				}
				case REQUEST_ATTR_NAME: {
					requestAttrName = charBuffer.toString();
					break;
				}
				case REQUEST_ATTR_VALUE: {
					requestAttrValue = charBuffer.toString();
					break;
				}
				default: {
					break;
				}
			}
		}catch(final Exception ex){
			throw new SAXParseException(ex.getMessage(), locator, ex);
		}
		state = stateStack.remove(stateStack.size() - 1);
		charBuffer.setLength(0);
	}
	
	/**
	 * <p>
	 * 	Lazily builds the locator String, in case logging isn't enabled.
	 * </p>
	 */
	protected Object getLocatorFormatter(){
		return new Object(){
			final Locator locator = XmlConfigurator.this.locator;
			
			@Override
			public String toString(){
				return "[line: " + locator.getLineNumber() + ", column: " + locator.getColumnNumber() + "]";
			}
		};
	}
	
	/**
	 * <p>
	 * 	Lazily builds the locator String, in case logging isn't enabled.
	 * 	({@link SAXParseException}) should just implement {@link Locator}...)
	 * </p>
	 */
	protected Object getLocatorFormatter(final SAXParseException spe){
		return new Object(){
			@Override
			public String toString(){
				return "[line: " + spe.getLineNumber() + ", column: " + spe.getColumnNumber() + "]";
			}
		};
	}
	
}