Macro.java

/**
 * 
 */
package edu.odu.cs.cowem.macroproc;

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

/**
 * A text substitution macro
 * 
 * Macros can be called as name or name(arg1, arg2, ...)
 * 
 * The ( ) in a macro call can be replaced by [ ] or { } or < > 
 *  
 * Macro calls are replaced by the macro body, with substitution for the
 * macro parameters. A macro substitution also swallows a single blank
 * preceding the macro name if the macro name starts with an alphanumeric
 * character and if the call is not at the beginning of the string)
 * and a single blank following the macro call if the
 * macro call has no ( ), [ ], { }, or < >.
 * 
 * @author zeil
 *
 */
public class Macro {
	
    /**
     * Name of this macro.
     */
	private String name;
	
	/**
	 * List of parameter names for this macro.
	 */
	private List<String> formalParams;
	
	/**
	 * The body (substitution part) of this macro.
	 */
	private String body;
	
	/**
	 * Create a new macro.
	 * @param name0  name of the macro - can be any string of visible characters
	 * @param params list of formal parameter names. Names should be limited
	 *               to alphanumerics and - _
	 * @param body0  body in which to replace params as a replacement for
	 *                the macro call
	 */
	public Macro (final String name0,  final List<String> params,  
	              final String body0) {
		this.name = name0;
		this.formalParams = new ArrayList<String>();
		this.formalParams.addAll(params);
		this.body = body0;
	}

	/**
	 * Create a new macro with zero parameters.
	 * @param name0  name of the macro - can be any string of visible characters
	 * @param body0  body to use as a replacement for the macro call
	 */
	public Macro (final String name0,  final String body0) {
		this.name = name0;
		this.formalParams = new ArrayList<String>();
		this.body = body0;
		
	}
	
	/**
	 * Apply the macro to a string .
	 * @param target0 string to be processed via this macro. 
	 * @return target0 with macro substitutions, unchanged if macro call
	 *           could not be matched
	 */
	public final String apply (final String target0) {
	    String target = target0;
		int start = 0;
		while (start >= 0 && start < target.length()) {
			int pos = target.indexOf(name, start);
			if (pos < 0) {
				break;
			}
			char firstCharInName = name.charAt(0);
			if (pos > start && Character.isLetterOrDigit(firstCharInName) 
					&& target.charAt(pos - 1) != ' ') {
				start = pos + 1;
				continue;
			}

			char opener = ' ';
			if (pos + name.length() < target.length()) {
				opener = target.charAt(pos + name.length());
			}
			char closer = ' ';
			switch (opener) {
			case '(' : closer = ')'; break;
			case '[' : closer = ']'; break;
			case '{' : closer = '}'; break;
			case '<' : closer = '>'; break;
			default:
			}
			// If closer == ' ', we must be looking at a zero-parmaeter
			// macro.
			if (closer == ' ' && formalParams.size() > 0) {
				// Not a valid macro call
				start = pos + 1;
				continue;
			}
			String[] actualParams;
			int callStart = 0;
			if (pos == start) {
				callStart = start;
            } else if (Character.isLetterOrDigit(firstCharInName)) {
				callStart = pos - 1;
			} else {
				callStart = pos;
			}
			String preMatch = target.substring(0, Math.max(0, callStart));
			String postMatch;
			if (closer == ' ') {
				actualParams = new String[0];
				postMatch = target.substring(Math.min(pos + name.length(),
				                             target.length()));
			} else {
				int closePos = target.indexOf(closer, pos + name.length() + 1);
				if (closePos < 0) {
					start = pos + 1;
					continue;
				}
				postMatch = target.substring(closePos + 1);
				String args = target.substring(pos + name.length() + 1,
				                               closePos);
				if (formalParams.size() > 1) {
				    final int splitLimit = 20;
					actualParams = args.split(",", splitLimit);
				} else {
					actualParams = new String[1];
					actualParams[0] = args;
				}
			}
			if (formalParams.size() == 0 && actualParams.length > 1) {
                start = pos + 1;
                continue;			    
			}
            if (formalParams.size() == 0 && actualParams.length == 1
                    && !actualParams[0].equals("")) {
                start = pos + 1;
                continue;               
            }
			if (formalParams.size() > 0 
			        && actualParams.length != formalParams.size()) {
				start = pos + 1;
				continue;
			}
			String replacement = applySubstitutions(actualParams);
			target = preMatch + replacement + postMatch;
			start = preMatch.length() + replacement.length();
		}
		return target;
	}

	/**
	 * Generate a version of the macro body with substitutions for all formal
	 * parameters.
	 * 
	 * @param actualParams list of values to substitute for the corresponding
	 *                     formal parameters.
	 * @return  Macro body with substitutions.
	 */
	private String applySubstitutions(final String[] actualParams) {
		String replacement = body;
		int start = 0;
		while (start < replacement.length()) {
			int pos = replacement.length();
			int matched = -1;
			for (int i = 0; i < formalParams.size(); ++i) {
				String formal = formalParams.get(i);
				int k = replacement.indexOf(formal, start);
				if (k >= 0 && k < pos) {
					pos = k;
					matched = i;
				}
			}
			if (matched < 0) {
			    break;
			}
			replacement = replacement.substring(0, pos)
					+ actualParams[matched]
					+ replacement.substring(pos 
					        + formalParams.get(matched).length());
			start = pos + actualParams[matched].length() + 1;
		}
		return replacement;
	}
	
	/**
	 * Render the macro as a string.
	 * @return a readable version of the macro
	 */
	public final String toString() {
		return name + "{" + formalParams + "}=>" + body;
	}

    /**
     * @return the name
     */
    public final String getName() {
        return name;
    }

    /**
     * @param name0 the name to set
     */
    public final void setName(final String name0) {
        this.name = name0;
    }

    /**
     * @return the formalParams
     */
    public final List<String> getFormalParams() {
        return formalParams;
    }

    /**
     * @param formalParams0 the formalParams to set
     */
    public final void setFormalParams(final List<String> formalParams0) {
        this.formalParams = new ArrayList<String>(formalParams0);
    }

    /**
     * @return the body
     */
    public final String getBody() {
        return body;
    }

    /**
     * @param body0 the body to set
     */
    public final void setBody(final String body0) {
        this.body = body0;
    }

}