LangUtil.java

/* *******************************************************************
 * Copyright (c) 1999-2026 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:
 *     Xerox/PARC     initial implementation
 *     Andy Clement
 *
 * SPDX-License-Identifier: EPL-2.0
 * ******************************************************************/
package org.aspectj.testing.util;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.IMessage;

/**
 * Misc lang utilities
 */
public class LangUtil {

	/** Delimiter used by split(String) (and ArrayList.toString()?) */
	public static final String SPLIT_DELIM = ", ";

	/** prefix used by split(String) (and ArrayList.toString()?) */
	public static final String SPLIT_START = "[";

	/** suffix used by split(String) (and ArrayList.toString()?) */
	public static final String SPLIT_END = "]";

	/** system-dependent classpath separator */
	public static final String CLASSPATH_SEP;

	private static final String[] NONE = new String[0];

	/** bad: hard-wired unix, windows, mac path separators */
	private static final char[] SEPS = new char[] { '/', '\\', ':' };

	static {
		// XXX this has to be the wrong way to get system-dependent classpath separator
		String ps = ";";
		try {
			ps = System.getProperty("path.separator");
			if (null == ps) {
				ps = ";";
				String cp = System.getProperty("java.class.path");
				if (null != cp) {
					if (cp.contains(";")) {
						ps = ";";
					} else if (cp.contains(":")) {
						ps = ":";
					}
					// else warn?
				}
			}
		} catch (Throwable t) { // ignore
		} finally {
			CLASSPATH_SEP = ps;
		}
	}

	/**
	 * @return input if any are empty or no target in input, or input with escape
	 *         prefixing all original target
	 */
	public static String escape(String input, String target, String escape) {
		if (isEmpty(input) || isEmpty(target) || isEmpty(escape)) {
			return input;
		}
		StringBuffer sink = new StringBuffer();
		escape(input, target, escape, sink);
		return sink.toString();
	}

	/**
	 * Append escaped input to sink. Cheap form of arbitrary escaping does not
	 * escape the escape String itself, but unflatten treats it as significant only
	 * before the target. (so this fails with input that ends with target).
	 */
	public static void escape(String input, String target, String escape, StringBuffer sink) {
		if ((null == sink) || isEmpty(input) || isEmpty(target) || isEmpty(escape)) {
			return;
		} else if (!input.contains(target)) { // avoid StringTokenizer construction
			sink.append(input);
			return;
		}
		throw new Error("unimplemented");
	}

	/** flatten list per spec to sink */
	public static void flatten(List<String> list, FlattenSpec spec, StringBuffer sink) {
		throwIaxIfNull(spec, "spec");
		final FlattenSpec s = spec;
		flatten(list, s.prefix, s.nullFlattened, s.escape, s.delim, s.suffix, sink);
	}

	/**
	 * Flatten a List to String by first converting to String[] (using toString() if
	 * the elements are not already String) and calling flatten(String[]...).
	 */
	public static void flatten(List<String> list, String prefix, String nullFlattened, String escape, String delim,
			String suffix, StringBuffer sink) {
		throwIaxIfNull(list, "list");
		Object[] ra = list.toArray();
		String[] result;
		if (String.class == ra.getClass().getComponentType()) {
			result = (String[]) ra;
		} else {
			result = new String[ra.length];
			for (int i = 0; i < result.length; i++) {
				if (null != ra[i]) {
					result[i] = ra[i].toString();
				}
			}
		}
		flatten(result, prefix, nullFlattened, escape, delim, suffix, sink);
	}

	/** flatten String[] per spec to sink */
	public static void flatten(String[] input, FlattenSpec spec, StringBuffer sink) {
		throwIaxIfNull(spec, "spec");
		final FlattenSpec s = spec;
		flatten(input, s.prefix, s.nullFlattened, s.escape, s.delim, s.suffix, sink);
	}

	/**
	 * Flatten a String[] to String by writing strings to sink, prefixing with
	 * leader (if not null), using nullRendering for null entries (skipped if null),
	 * escaping any delim in entry by prefixing with escape (if not null),
	 * separating entries with delim (if not null), and suffixing with trailer (if
	 * not null). Note that nullFlattened is not processed for internal delim, and
	 * strings is not copied before processing.
	 * 
	 * @param strings       the String[] input - not null
	 * @param prefix        the output starts with this if not null
	 * @param nullFlattened the output of a null entry - entry is skipped (no delim)
	 *                      if null
	 * @param escape        any delim in an item will be prefixed by escape if not
	 *                      null
	 * @param delim         two items in the output will be separated by delim if
	 *                      not null
	 * @param suffix        the output ends with this if not null
	 * @param sink          the StringBuffer to use for output
	 * @return null if sink is not null (results added to sink) or rendering
	 *         otherwise
	 */
	public static void flatten(String[] strings, String prefix, String nullFlattened, String escape, String delim,
			String suffix, StringBuffer sink) {
		throwIaxIfNull(strings, "strings");
		if (null == sink) {
			return;
		}
		final boolean haveDelim = (!isEmpty(delim));
		final boolean haveNullFlattened = (null != nullFlattened);
		final boolean escaping = (haveDelim && (null != escape));
		final int numStrings = (null == strings ? 0 : strings.length);
		if (null != prefix) {
			sink.append(prefix);
		}
		for (int i = 0; i < numStrings; i++) {
			String s = strings[i];
			if (null == s) {
				if (!haveNullFlattened) {
					continue;
				}
				if (haveDelim && (i > 0)) {
					sink.append(delim);
				}
				sink.append(nullFlattened);
			} else {
				if (haveDelim && (i > 0)) {
					sink.append(delim);
				}

				if (escaping) {
					escape(s, delim, escape, sink);
				} else {
					sink.append(s);
				}
			}
		}
		if (null != suffix) {
			sink.append(suffix);
		}
	}

	/**
	 * Get indexes of any invalid entries in array.
	 * 
	 * @param ra        the Object[] entries to check (if null, this returns new
	 *                  int[] { -1 })
	 * @param superType the Class, if any, to verify that any entries are
	 *                  assignable.
	 * @return null if all entries are non-null, assignable to superType or
	 *         comma-delimited error String, with components
	 *         <code>"[#] {null || not {superType}"</code>, e.g., "[3] null, [5] not
	 *         String"
	 */
	public static String invalidComponents(Object[] ra, Class superType) {
		if (null == ra) {
			return "null input array";
		} else if (0 == ra.length) {
			return null;
		}
		StringBuilder result = new StringBuilder();
		final String cname = LangUtil.unqualifiedClassName(superType);
//        int index = 0;
		for (int i = 0; i < ra.length; i++) {
			if (null == ra[i]) {
				result.append(", [" + i + "] null");
			} else if ((null != superType) && !superType.isAssignableFrom(ra[i].getClass())) {
				result.append(", [" + i + "] not " + cname);
			}
		}
		if (0 == result.length()) {
			return null;
		} else {
			return result.toString().substring(2);
		}
	}

	/** @return ((null == ra) || (0 == ra.length)) */
	public static boolean isEmpty(Object[] ra) {
		return ((null == ra) || (0 == ra.length));
	}

	/** @return ((null == s) || (0 == s.length())); */
	public static boolean isEmpty(String s) {
		return ((null == s) || (0 == s.length()));
	}

	/**
	 * Throw IllegalArgumentException if any component in input array is null or (if
	 * superType is not null) not assignable to superType. The exception message
	 * takes the form <code>{name} invalid entries: {invalidEntriesResult}</code>
	 * 
	 * @throws IllegalArgumentException if any components bad
	 * @see #invalidComponents(Object[], Class)
	 */
	public static final void throwIaxIfComponentsBad(final Object[] input, final String name, final Class superType) {
		String errs = invalidComponents(input, superType);
		if (null != errs) {
			String err = name + " invalid entries: " + errs;
			throw new IllegalArgumentException(err);
		}
	}

	/**
	 * Shorthand for "if false, throw IllegalArgumentException"
	 * 
	 * @throws IllegalArgumentException "{message}" if test is false
	 */
	public static final void throwIaxIfFalse(final boolean test, final String message) {
		if (!test) {
			throw new IllegalArgumentException(message);
		}
	}

	/**
	 * Shorthand for "if null, throw IllegalArgumentException"
	 * 
	 * @throws IllegalArgumentException "null {name}" if o is null
	 */
	public static final void throwIaxIfNull(final Object o, final String name) {
		if (null == o) {
			String message = "null " + (null == name ? "input" : name);
			throw new IllegalArgumentException(message);
		}
	}

	public static List<String> unflatten(String input, FlattenSpec spec) {
		throwIaxIfNull(spec, "spec");
		final FlattenSpec s = spec;
		return unflatten(input, s.prefix, s.nullFlattened, s.escape, s.delim, s.suffix, s.emptyUnflattened);
	}

	/**
	 * Unflatten a String to String[] by separating elements at delim, handling
	 * prefixes, suffixes, escapes, etc. Any prefix or suffix is stripped from the
	 * input (or, if not found, an IllegalArgumentException is thrown). If delim is
	 * null or empty or input contains no delim, then return new String[] {stripped
	 * input}.
	 *
	 * XXX fix comments prefixing with leader (if not null), using nullRendering for
	 * null entries (skipped if null), escaping any delim in entry by prefixing with
	 * escape (if not null), separating entries with delim (if not null), and
	 * suffixing with trailer (if not null). Note that nullRendering is not
	 * processed for internal delim, and strings is not copied before processing.
	 * 
	 * @param strings       the String[] input - not null
	 * @param prefix        the output starts with this if not null
	 * @param nullRendering the output of a null entry - entry is skipped (no delim)
	 *                      if null
	 * @param escape        any delim in an item will be prefixed by escape if not
	 *                      null
	 * @param delim         two items in the output will be separated by delim if
	 *                      not null
	 * @param suffix        the output ends with this if not null
	 * @param sink          the StringBuffer to use for output
	 * @return null if sink is not null (results added to sink) or rendering
	 *         otherwise
	 * @throws IllegalArgumentException if input is null or if any prefix does not
	 *                                  start the input or if any suffix does not
	 *                                  end the input
	 */
	public static List<String> unflatten(String input, String prefix, String nullFlattened, String escape, String delim,
			String suffix, String emptyUnflattened) {
		throwIaxIfNull(input, "input");
		final boolean haveDelim = (!isEmpty(delim));
//        final boolean haveNullFlattened = (null != nullFlattened);
//        final boolean escaping = (haveDelim && (null != escape));
		if (!isEmpty(prefix)) {
			if (input.startsWith(prefix)) {
				input = input.substring(prefix.length());
			} else {
				String s = "expecting \"" + prefix + "\" at start of " + input + "\"";
				throw new IllegalArgumentException(s);
			}
		}
		if (!isEmpty(suffix)) {
			if (input.endsWith(suffix)) {
				input = input.substring(0, input.length() - suffix.length());
			} else {
				String s = "expecting \"" + suffix + "\" at end of " + input + "\"";
				throw new IllegalArgumentException(s);
			}
		}

		final List<String> result = new ArrayList<>();
		if (isEmpty(input)) {
			return result;
		}
		if ((!haveDelim) || (!input.contains(delim))) {
			result.add(input);
			return result;
		}

		StringTokenizer st = new StringTokenizer(input, delim, true);
//        StringBuffer cur = new StringBuffer();
//        boolean lastEndedWithEscape = false;
//        boolean lastWasDelim = false;
		while (st.hasMoreTokens()) {
			String token = st.nextToken();
			System.out.println("reading " + token);
			if (delim.equals(token)) {
			} else {
				result.add(token);
			}
		}
		return result;
	}

	/**
	 * combine two string arrays, removing null and duplicates
	 * 
	 * @return concatenation of both arrays, less null in either or dups in two
	 * @see Util#combine(Object[], Object[])
	 */
	public static String[] combine(String[] one, String[] two) {
		List<String> twoList = new ArrayList<>(org.aspectj.util.LangUtil.arrayAsList(two));
		ArrayList<String> result = new ArrayList<>();
		if (null != one) {
			for (String s : one) {
				if (null != s) {
					twoList.remove(s);
					result.add(s);
				}
			}
		}
		for (Object o : twoList) {
			String element = (String) o;
			if (null != element) {
				result.add(element);
			}
		}
		return (String[]) result.toArray(NONE);
	}

	public static Properties combine(Properties dest, Properties add, boolean respectExisting) { // XXX
		if (null == add)
			return dest;
		if (null == dest)
			return add;
		for (Object o : add.keySet()) {
			String key = (String) o;
			if (null == key) {
				continue;
			}
			String value = add.getProperty(key);
			if (null == value) {
				continue;
			}
			if (!respectExisting || (null == dest.getProperty(key))) {
				dest.setProperty(key, value);
			}
		}
		return dest;
	}

	public static List arrayAsList(Object[] ra) {
		return org.aspectj.util.LangUtil.arrayAsList(ra);
	}

	/**
	 * return the fully-qualified class names inferred from the file names in dir
	 * assuming dir is the root of the source tree and class files end with
	 * ".class".
	 * 
	 * @throws Error if dir is not properly named as prefix of class files found in
	 *               dir.
	 */
	public static String[] classesIn(File dir) {
		boolean alwaysTrue = true;
		FileFilter filter = ValidFileFilter.CLASS_FILE;
		CollectorFileFilter collector = new CollectorFileFilter(filter, alwaysTrue);
		FileUtil.descendFileTree(dir, collector);
		List<File> list = collector.getFiles();
		String[] result = new String[list.size()];
		Iterator<File> it = list.iterator();
		String dirPrefix = dir.getPath();
		for (int i = 0; i < result.length; i++) {
			if (!it.hasNext()) {
				throw new Error("unexpected end of list at " + i);
			}
			result[i] = fileToClassname((File) it.next(), dirPrefix);
		}
		return result;
	}

	/**
	 * Convert String[] to String by using conventions for split. Will ignore any
	 * entries containing SPLIT_DELIM (and write as such to errs if not null).
	 * 
	 * @param input the String[] to convert
	 * @param errs  the StringBuffer for error messages (if any)
	 */
	public static String unsplit(String[] input, StringBuffer errs) {
		StringBuilder sb = new StringBuilder();
		sb.append(SPLIT_START);
		for (int i = 0; i < input.length; i++) {
			if (input[i].contains(SPLIT_DELIM)) {
				if (null != errs) {
					errs.append("\nLangUtil.unsplit(..) - item " + i + ": \"" + input[i] + " contains \"" + SPLIT_DELIM
							+ "\"");
				}
			} else {
				sb.append(input[i]);
				if (1 + i < input.length) {
					sb.append(SPLIT_DELIM);
				}
			}
		}
		sb.append(SPLIT_END);
		return sb.toString();

	}

	/**
	 * Split input into substrings on the assumption that it is either only one
	 * string or it was generated using List.toString(), with tokens
	 * 
	 * <pre>
	 * SPLIT_START {string} { SPLIT_DELIM {string}} SPLIT_END
	 * 
	 * <pre>
	 * (e.g., <code>"[one, two, three]"</code>).
	 */
	public static String[] split(String s) {
		if (null == s) {
			return null;
		}
		if ((!s.startsWith(SPLIT_START)) || (!s.endsWith(SPLIT_END))) {
			return new String[] { s };
		}
		s = s.substring(SPLIT_START.length(), s.length() - SPLIT_END.length());
		final int LEN = s.length();
		int start = 0;
		final List<String> result = new ArrayList<>();
		final String DELIM = ", ";
		int loc = s.indexOf(SPLIT_DELIM, start);
		while ((start < LEN) && (-1 != loc)) {
			result.add(s.substring(start, loc));
			start = DELIM.length() + loc;
			loc = s.indexOf(SPLIT_DELIM, start);
		}
		result.add(s.substring(start));
		return (String[]) result.toArray(new String[0]);
	}

	public static String[] strip(String[] src, String[] toStrip) {
		if (null == toStrip) {
			return strip(src, NONE);
		} else if (null == src) {
			return strip(NONE, toStrip);
		}
		List<String> slist = org.aspectj.util.LangUtil.arrayAsList(src);
		List<String> tlist = org.aspectj.util.LangUtil.arrayAsList(toStrip);
		slist.removeAll(tlist);
		return (String[]) slist.toArray(NONE);
	}

	/**
	 * Load all classes specified by args, logging success to out and fail to err.
	 */
	public static void loadClasses(String[] args, StringBuffer out, StringBuffer err) {
		if (null != args) {
			for (String arg : args) {
				try {
					Class<?> c = Class.forName(arg);
					if (null != out) {
						out.append("\n");
						out.append(arg);
						out.append(": ");
						out.append(c.getName());
					}
				} catch (Throwable t) {
					if (null != err) {
						err.append("\n");
						FileUtil.render(t, err);
					}
				}
			}

		}
	}

	private static String fileToClassname(File f, String prefix) {
		// this can safely assume file exists, starts at base, ends with .class
		// this WILL FAIL if full path with drive letter on windows
		String path = f.getPath();
		if (!path.startsWith(prefix)) {
			String err = "!\"" + path + "\".startsWith(\"" + prefix + "\")";
			throw new IllegalArgumentException(err);
		}
		int length = path.length() - ".class".length();
		path = path.substring(prefix.length() + 1, length);
		for (char sep : SEPS) {
			path = path.replace(sep, '.');
		}
		return path;
	}

	public static void main(String[] args) { // todo remove as testing
		StringBuffer err = new StringBuffer();
		StringBuffer out = new StringBuffer();
		for (String arg : args) {
			String[] names = classesIn(new File(arg));
			System.err.println(arg + " -> " + render(names));
			loadClasses(names, out, err);
		}
		if (0 < err.length()) {
			System.err.println(err.toString());
		}
		if (0 < out.length()) {
			System.out.println(out.toString());
		}
	}

	public static String render(String[] args) { // todo move as testing
		if ((null == args) || (1 > args.length)) {
			return "[]";
		}
		boolean longFormat = (args.length < 10);
		String sep = (longFormat ? ", " : "\n\t");
		StringBuilder sb = new StringBuilder();
		if (!longFormat)
			sb.append("[");
		for (int i = 0; i < args.length; i++) {
			if (0 < i)
				sb.append(sep);
			sb.append(args[i]);
		}
		sb.append(longFormat ? "\n" : "]");
		return sb.toString();
	}

	/**
	 * @param thrown the Throwable to render
	 */
	public static String debugStr(Throwable thrown) {
		if (null == thrown) {
			return "((Throwable) null)";
		} else if (thrown instanceof InvocationTargetException) {
			return debugStr(((InvocationTargetException) thrown).getTargetException());
		} else if (thrown instanceof AbortException) {
			IMessage m = ((AbortException) thrown).getIMessage();
			if (null != m) {
				return "" + m;
			}
		}
		StringWriter buf = new StringWriter();
		PrintWriter writer = new PrintWriter(buf);
		writer.println(thrown.getMessage());
		thrown.printStackTrace(writer);
		try {
			buf.close();
		} catch (IOException ioe) {
		}
		return buf.toString();
	}

	/**
	 * <code>debugStr(o, false);</code>
	 * 
	 * @param source the Object to render
	 */
	public static String debugStr(Object o) {
		return debugStr(o, false);
	}

	/**
	 * Render standard debug string for an object in normal, default form.
	 * 
	 * @param source  the Object to render
	 * @param recurse if true, then recurse on all non-primitives unless rendered
	 */
	public static String debugStr(Object o, boolean recurse) {
		if (null == o) {
			return "null";
		} else if (recurse) {
			List<Object> rendering = new ArrayList<>();
			rendering.add(o);
			return debugStr(o, rendering);
		} else {
			Class<?> c = o.getClass();
			Field[] fields = c.getDeclaredFields();
			Object[] values = new Object[fields.length];
			String[] names = new String[fields.length];
			for (int i = 0; i < fields.length; i++) {
				Field field = fields[i];
				names[i] = field.getName();
				try {
					values[i] = field.get(o);
					if (field.getType().isArray()) {
						List list = org.aspectj.util.LangUtil.arrayAsList((Object[]) values[i]);
						values[i] = list.toString();
					}
				} catch (IllegalAccessException e) {
					values[i] = "<IllegalAccessException>";
				}
			}
			return debugStr(c, names, values);
		}
	}

	/**
	 * recursive variant avoids cycles. o added to rendering before call.
	 */
	private static String debugStr(Object o, List<Object> rendering) {
		if (null == o) {
			return "null";
		} else if (!rendering.contains(o)) {
			throw new Error("o not in rendering");
		}
		Class<?> c = o.getClass();
		if (c.isArray()) {
			Object[] ra = (Object[]) o;
			StringBuilder sb = new StringBuilder();
			sb.append("[");
			for (int i = 0; i < ra.length; i++) {
				if (i > 0) {
					sb.append(", ");
				}
				rendering.add(ra[i]);
				sb.append(debugStr(ra[i], rendering));
			}
			sb.append("]");
			return sb.toString();
		}
		Field[] fields = nonStaticFields(c.getFields());
		Object[] values = new Object[fields.length];
		String[] names = new String[fields.length];
		for (int i = 0; i < fields.length; i++) {
			Field field = fields[i];
			names[i] = field.getName();
			// collapse to String
			Object value = privilegedGetField(field, o);
			if (null == value) {
				values[i] = "null";
			} else if (rendering.contains(value)) {
				values[i] = "<recursion>";
			} else {
				rendering.add(value);
				values[i] = debugStr(value, rendering);
			}
		}
		return debugStr(c, names, values);
	}

	/** incomplete - need protection domain */
	private static Object privilegedGetField(final Field field, final Object o) {
		try {
			return AccessController.doPrivileged(new PrivilegedExceptionAction() {
				public Object run() {
					try {
						return field.get(o);
					} catch (IllegalAccessException e) {
						return "<IllegalAccessException>";
					}
				}
			});
		} catch (PrivilegedActionException e) {
			return "<IllegalAccessException>";
		}
	}

	private static Field[] nonStaticFields(Field[] fields) {
		if (null == fields) {
			return new Field[0];
		}
		int to = 0;
		int from = 0;
		while (from < fields.length) {
			if (!Modifier.isStatic(fields[from].getModifiers())) {
				if (to != from) {
					fields[to] = fields[from];
				}
				to++;
			}
			from++;
		}
		if (to < from) {
			Field[] result = new Field[to];
			if (to > 0) {
				System.arraycopy(fields, 0, result, 0, to);
			}
			fields = result;
		}
		return fields;
	}

	/** <code> debugStr(source, names, items, null, null, null, null)<code> */
	public static String debugStr(Class source, String[] names, Object[] items) {
		return debugStr(source, null, names, null, items, null, null);
	}

	/**
	 * Render standard debug string for an object. This is the normal form and an
	 * example with the default values:
	 * 
	 * <pre>
	 * {className}{prefix}{{name}{infix}{value}{delimiter}}..{suffix}
	 * Structure[head=root, tail=leaf]
	 * </pre>
	 * 
	 * Passing null for the formatting entries provokes the default values, so to
	 * print nothing, you should pass "". Default values:
	 * 
	 * <pre>
	 * prefix: "[" SPLIT_START infix: "=" delimiter: ", " SPLIT_DELIM suffix: "]"
	 * SPLIT_END
	 * 
	 * @param source    the Class prefix to render unqualified - omitted if null
	 * @param names     the String[] (field) names of the items - omitted if null
	 * @param items     the Object[] (field) values
	 * @param prefix    the String to separate classname and start of name/values
	 * @param delimiter the String to separate name/value instances
	 * @param infix     the String to separate name and value used only if both name
	 *                  and value exist
	 * @param suffix    the String to delimit the end of the name/value instances
	 *                  used only if classname exists
	 */
	public static String debugStr(Class source, String prefix, String[] names, String infix, Object[] items,
			String delimiter, String suffix) {

		if (null == delimiter) {
			delimiter = SPLIT_DELIM;
		}
		if (null == prefix) {
			prefix = SPLIT_START;
		}
		if (null == infix) {
			infix = "=";
		}
		if (null == suffix) {
			suffix = SPLIT_END;
		}
		StringBuilder sb = new StringBuilder();
		if (null != source) {
			sb.append(org.aspectj.util.LangUtil.unqualifiedClassName(source));
		}
		sb.append(prefix);
		if (null == names) {
			names = NONE;
		}
		if (null == items) {
			items = NONE;
		}
		final int MAX = (names.length > items.length ? names.length : items.length);
		for (int i = 0; i < MAX; i++) {
			if (i > 0) {
				sb.append(delimiter);
			}
			if (i < names.length) {
				sb.append(names[i]);
			}
			if (i < items.length) {
				if (i < names.length) {
					sb.append(infix);
				}
				sb.append(items[i] + "");
			}
		}
		sb.append(suffix);
		return sb.toString();
	}

	/**
	 * @return a String with the unqualified class name of the object (or "null")
	 */
	public static String unqualifiedClassName(Object o) {
		return unqualifiedClassName(null == o ? null : o.getClass());
	}

	/**
	 * @return a String with the unqualified class name of the class (or "null")
	 */
	public static String unqualifiedClassName(Class c) {
		if (null == c) {
			return "null";
		}
		String name = c.getName();
		int loc = name.lastIndexOf(".");
		if (-1 != loc)
			name = name.substring(1 + loc);
		return name;
	}

	/**
	 * Calculate exact diffs and report missing and extra items. This assumes the
	 * input List are not modified concurrently.
	 * 
	 * @param expectedListIn the List of expected results - treated as empty if null
	 * @param actualListIn   the List of actual results - treated as empty if null
	 * @param extraListOut   the List for any actual results not expected - ignored
	 *                       if null
	 * @param missingListOut the List for any expected results not found - ignored
	 *                       if null
	 */
	public static <T> void makeDiffs(List<T> expectedListIn, List<T> actualListIn, List<T> missingListOut, List<T> extraListOut) {
		if ((null == missingListOut) && (null == extraListOut)) {
			return;
		}
		if (null == expectedListIn) {
			expectedListIn = Collections.emptyList();
		}
		if (null == actualListIn) {
			actualListIn = Collections.emptyList();
		}
		if ((0 == actualListIn.size()) && (0 == expectedListIn.size())) {
			return;
		}
		BitSet actualExpected = new BitSet();
		for (T expect : expectedListIn) {
			int loc = actualListIn.indexOf(expect);
			if (-1 == loc) {
				if (null != missingListOut) {
					missingListOut.add(expect);
				}
			} else {
				actualExpected.set(loc);
			}
		}
		if (null != extraListOut) {
			for (int i = 0; i < actualListIn.size(); i++) {
				if (!actualExpected.get(i)) {
					extraListOut.add(actualListIn.get(i));
				}
			}
		}
	}

	// XXX unit test for makeSoftDiffs
	/**
	 * Calculate potentially "soft" diffs using Comparator.compare(expected,
	 * actual). This shallow-copies and sorts the input Lists.
	 * 
	 * @param expectedListIn the List of expected results - treated as empty if null
	 * @param actualListIn   the List of actual results - treated as empty if null
	 * @param extraListOut   the List for any actual results not expected - ignored
	 *                       if null
	 * @param missingListOut the List for any expected results not found - ignored
	 *                       if null
	 * @param comparator     the Comparator for comparisons - not null
	 * @throws IllegalArgumentException if comp is null
	 */
	public static <T> void makeSoftDiffs( // XXX no intersect or union on collections???
			List<T> expectedListIn, List<T> actualListIn, List<T> missingListOut,
			List<T> extraListOut, Comparator<T> comparator) {
		if ((null == missingListOut) && (null == extraListOut)) {
			return;
		}
		if (null == comparator) {
			throw new IllegalArgumentException("null comparator");
		}
		if (null == expectedListIn) {
			expectedListIn = Collections.emptyList();
		}
		if (null == actualListIn) {
			actualListIn = Collections.emptyList();
		}
		if ((0 == actualListIn.size()) && (0 == expectedListIn.size())) {
			return;
		}

		List<T> expected = new ArrayList<>(expectedListIn);
		expected.sort(comparator);

		List<T> actual = new ArrayList<>(actualListIn);
		actual.sort(comparator);
		Iterator<T> actualIter = actual.iterator();
		T act = null;

		if (missingListOut != null) {
			missingListOut.addAll(expectedListIn);
		}
		if (extraListOut != null) {
			extraListOut.addAll(actualListIn);
		}

		// AMC: less efficient, but simplified implementation. Needed since messages can
		// now match on text content too, and the old algorithm did not cope with two
		// expected
		// messages on the same line, but with different text content.
		while (actualIter.hasNext()) {
			act = actualIter.next();
			for (T exp : expected) {
				// if actual matches expected remove actual from extraListOut, and
				// remove expected from missingListOut
				int diff = comparator.compare(exp, act);
				if (diff == 0) {
					extraListOut.remove(act);
					missingListOut.remove(exp);
				} else if (diff > 0) {
					// since list is sorted, there can be no more matches...
					break;
				}
			}
		}

//        while (((null != act) || actualIter.hasNext())
//             && ((null != exp) || expectedIter.hasNext())) {
//            if (null == act) {
//                act = actualIter.next();
//            }
//            if (null == exp) {
//                exp = expectedIter.next();
//            }
//            int diff = comparator.compare(exp, act);
//            if (0 > diff) {          // exp < act
//                if (null != missingListOut) {
//                    missingListOut.add(exp);
//                    exp = null;
//                }
//            } else if (0 < diff) {   // exp > act
//                if (null != extraListOut) {
//                    extraListOut.add(act);
//                    act = null;
//                }
//            } else { // got match of actual to expected
//                // absorb all actual matching expected (duplicates)
//                while ((0 == diff) && actualIter.hasNext()) {
//                    act = actualIter.next();
//                    diff = comparator.compare(exp, act);
//                }
//                if (0 == diff) {
//                    act = null;
//                }
//                exp = null;
//            }
//        }
//        if (null != missingListOut) {
//             if (null != exp) {
//                missingListOut.add(exp);
//             }
//            while (expectedIter.hasNext()) {
//                 missingListOut.add(expectedIter.next());
//            }
//        }
//        if (null != extraListOut) {
//            if (null != act) {
//                extraListOut.add(act);
//            }
//            while (actualIter.hasNext()) {
//                 extraListOut.add(actualIter.next());
//            }
//        }
	}

	public static class FlattenSpec {
		/**
		 * This tells unflatten(..) to throw IllegalArgumentException if it finds two
		 * contiguous delimiters.
		 */
		public static final String UNFLATTEN_EMPTY_ERROR = "empty items not permitted when unflattening";
		/**
		 * This tells unflatten(..) to skip empty items when unflattening (since null
		 * means "use null")
		 */
		public static final String UNFLATTEN_EMPTY_AS_NULL = "unflatten empty items as null";

		/**
		 * This tells unflatten(..) to skip empty items when unflattening (since null
		 * means "use null")
		 */
		public static final String SKIP_EMPTY_IN_UNFLATTEN = "skip empty items when unflattening";

		/**
		 * For Ant-style attributes: "item,item" (with escaped commas). There is no way
		 * when unflattening to distinguish values which were empty from those which
		 * were null, so all are unflattened as empty.
		 */
		public static final FlattenSpec COMMA = new FlattenSpec(null, "", "\\", ",", null, "") {
			public String toString() {
				return "FlattenSpec.COMMA";
			}
		};

		/** this attempts to mimic ((List)l).toString() */
		public static final FlattenSpec LIST = new FlattenSpec("[", "", null, ", ", "]", UNFLATTEN_EMPTY_ERROR) {
			public String toString() {
				return "FlattenSpec.LIST";
			}
		};

		/** how toString renders null values */
		public static final String NULL = "<null>";

		private static String r(String s) {
			return (null == s ? NULL : s);
		}

		public final String prefix;
		public final String nullFlattened;
		public final String escape;
		public final String delim;
		public final String suffix;
		public final String emptyUnflattened;
		private transient String toString;

		public FlattenSpec(String prefix, String nullRendering, String escape, String delim, String suffix,
				String emptyUnflattened) {
			this.prefix = prefix;
			this.nullFlattened = nullRendering;
			this.escape = escape;
			this.delim = delim;
			this.suffix = suffix;
			this.emptyUnflattened = emptyUnflattened;
			throwIaxIfNull(emptyUnflattened, "use UNFLATTEN_EMPTY_AS_NULL");
		}

		public String toString() {
			if (null == toString) {
				toString = "FlattenSpec(" + "prefix=" + r(prefix) + ", nullRendering=" + r(nullFlattened) + ", escape="
						+ r(escape) + ", delim=" + r(delim) + ", suffix=" + r(suffix) + ", emptyUnflattened="
						+ r(emptyUnflattened) + ")";
			}
			return toString;
		}
	}
} // class LangUtil

//  --------- java runs using Ant
//	/**
//	 * Run a Java command separately.
//	 * @param className the fully-qualified String name of the class
//     *         with the main method to run
//	 * @param classpathFiles the File to put on the classpath
//	 * @param args to the main method of the class
//     * @param outSink the PrintStream for the output stream - may be null
//	 */
//	public static void oldexecuteJava(
//		String className,
//		File[] classpathFiles,
//		String[] args,
//        PrintStream outSink) {
//        Project project = new Project();
//        project.setName("LangUtil.executeJava(" + className + ")");
//        Path classpath = new Path(project, classpathFiles[0].getAbsolutePath());
//        for (int i = 1; i < classpathFiles.length; i++) {
//			classpath.addExisting(new Path(project, classpathFiles[i].getAbsolutePath()));
//		}
//
//        Commandline cmds = new Commandline();
//        cmds.addArguments(new String[] {className});
//        cmds.addArguments(args);
//
//        ExecuteJava runner = new ExecuteJava();
//        runner.setClasspath(classpath);
//        runner.setJavaCommand(cmds);
//        if (null != outSink) {
//            runner.setOutput(outSink); // XXX todo
//        }
//        runner.execute(project);
//	}

//     public static void executeJava(
//        String className,
//        File dir,
//        File[] classpathFiles,
//        String[] args,
//        PrintStream outSink) {
//        StringBuffer sb = new StringBuffer();
//
//        sb.append("c:/apps/jdk1.3.1/bin/java.exe -classpath \"");
//        for (int i = 0; i < classpathFiles.length; i++) {
//            if (i < 0) {
//                sb.append(";");
//            }
//            sb.append(classpathFiles[i].getAbsolutePath());
//        }
//        sb.append("\" -verbose " + className);
//        for (int i = 0; i < args.length; i++) {
//            sb.append(" " + args[i]);
//		}
//        Exec exec = new Exec();
//        Project project = new Project();
//        project.setProperty("ant.home", "c:/home/wes/aj/aspectj/modules/lib/ant");
//        System.setProperty("ant.home", "c:/home/wes/aj/aspectj/modules/lib/ant");
//        exec.setProject(new Project());
//        exec.setCommand(sb.toString());
//        exec.setDir(dir.getAbsolutePath());
//        exec.execute();
//    }
//     public static void execJavaProcess(
//        String className,
//        File dir,
//        File[] classpathFiles,
//        String[] args,
//        PrintStream outSink) throws Throwable {
//        StringBuffer sb = new StringBuffer();
//
//        sb.append("c:\\apps\\jdk1.3.1\\bin\\java.exe -classpath \"");
//        for (int i = 0; i < classpathFiles.length; i++) {
//            if (i > 0) {
//                sb.append(";");
//            }
//            sb.append(classpathFiles[i].getAbsolutePath());
//        }
//        sb.append("\" -verbose " + className);
//        for (int i = 0; i < args.length; i++) {
//            sb.append(" " + args[i]);
//        }
//        String command = sb.toString();
//        System.err.println("launching process: " + command);
//        Process process = Runtime.getRuntime().exec(command);
//        // huh? err/out
//        InputStream errStream = null;
//        InputStream outStream = null;
//        Throwable toThrow = null;
//        int result = -1;
//        try {
//            System.err.println("waiting for process: " + command);
//            errStream = null; // process.getErrorStream();
//            outStream = null; // process.getInputStream(); // misnamed - out
//            result = process.waitFor();
//            System.err.println("Done waiting for process: " + command);
//            process.destroy();
//        } catch (Throwable t) {
//            toThrow = t;
//        } finally {
//            if (null != outStream) {
//                FileUtil.copy(outStream, System.out, false);
//                try { outStream.close(); }
//                catch (IOException e) {}
//            }
//            if (null != errStream) {
//                FileUtil.copy(errStream, System.err, false);
//                try { errStream.close(); }
//                catch (IOException e) {}
//            }
//        }
//        if (null != toThrow) {
//            throw toThrow;
//        }
//    }
//        try {
//            // show the command
//            log(command, Project.MSG_VERBOSE);
//
//            // exec command on system runtime
//            Process proc = Runtime.getRuntime().exec(command);
//
//            if (out != null)  {
//                fos = new PrintWriter(new FileWriter(out));
//                log("Output redirected to " + out, Project.MSG_VERBOSE);
//            }
//
//            // copy input and error to the output stream
//            StreamPumper inputPumper =
//                new StreamPumper(proc.getInputStream(), Project.MSG_INFO);
//            StreamPumper errorPumper =
//                new StreamPumper(proc.getErrorStream(), Project.MSG_WARN);
//
//            // starts pumping away the generated output/error
//            inputPumper.start();
//            errorPumper.start();
//
//            // Wait for everything to finish
//            proc.waitFor();
//            inputPumper.join();
//            errorPumper.join();
//            proc.destroy();
//
//            // close the output file if required
//            logFlush();
//
//            // check its exit value
//            err = proc.exitValue();
//            if (err != 0) {
//                if (failOnError) {
//                    throw new BuildException("Exec returned: " + err, getLocation());
//                } else {
//                    log("Result: " + err, Project.MSG_ERR);
//                }
//            }
//        } catch (IOException ioe) {
//            throw new BuildException("Error exec: " + command, ioe, getLocation());
//        } catch (InterruptedException ex) {}
//