FluentIssueCollection.java

package com.itemis.xtext.testing;

import static com.itemis.xtext.testing.XtextUtils.ancestor;
import static com.itemis.xtext.testing.XtextUtils.eString;
import static com.itemis.xtext.testing.XtextUtils.egetAndResolve;
import static com.itemis.xtext.testing.XtextUtils.getEObject;
import static com.itemis.xtext.testing.XtextUtils.name;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.diagnostics.Severity;
import org.eclipse.xtext.validation.Issue;

import com.google.common.collect.Iterables;

/**
 * Offers a fluent way of asserting Xtext Issues (Validation Warnings and
 * Errors).
 *
 * @author Markus Voelter - Initial Contribution and API
 * @author Karsten Thoms
 * @author Lars Corneliussen
 */
public class FluentIssueCollection implements Iterable<Issue> {

	private static Logger LOGGER = Logger.getLogger(FluentIssueCollection.class);

	private final List<Issue> issues;
	private final List<String> messages;
	private final Resource resource;

	private boolean state;
	private boolean stateIsSet;

	public FluentIssueCollection(final Resource res, final List<Issue> issues, final List<String> messages) {
		resource = res;
		this.issues = issues;
		this.messages = messages;
	}

	public FluentIssueCollection(final Resource res, final List<String> messages) {
		issues = new ArrayList<Issue>();
		resource = res;
		this.messages = messages;
	}

	private void addMessage(final String m) {
		messages.add(m);
	}

	public void addIssue(final Issue issue) {
		issues.add(issue);
	}

	public FluentIssueCollection forType(final Class<? extends EObject> cls) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		for (final Issue i : issues) {
			final URI uri = i.getUriToProblem();
			final EObject eObject = resource.getEObject(uri.fragment());
			if (cls.isInstance(eObject)) {
				res.addIssue(i);
			}
		}
		if (res.getIssueCount() == 0) {
			res.addMessage("No issues found for type " + cls.getName());
		}
		return res;
	}

	public FluentIssueCollection get(final int index) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		if (index >= getIssueCount()) {
			res.addMessage("trying to get element at " + index + ", but only have " + getIssueCount()
					+ " elements -> creating empty collection!");
		} else {
			res.addIssue(getIssues().get(index));
		}
		return res;
	}

	public FluentIssueCollection inLine(final int lineNo) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		int rc = 0;
		for (final Issue i : issues) {
			if (i.getLineNumber() == lineNo) {
				res.addIssue(i);
				rc++;
			}
		}
		if (rc == 0) {
			res.addMessage("no issues found for line number " + lineNo);
		}
		return res;
	}

	public FluentIssueCollection withStringFeatureValue(final String featureName, final String value) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		for (final Issue i : issues) {
			final EObject eObject = getEObject(i, resource);
			final String v = eString(egetAndResolve(eObject, featureName, resource.getResourceSet()));
			if (v.contains(value)) {
				res.addIssue(i);
			}
		}
		if (res.getIssueCount() == 0) {
			res.addMessage("no elements found with feature " + featureName + " valued '" + value + "'");
		}
		return res;
	}

	public FluentIssueCollection except(final Set<Issue> toBeRemoved) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);

		if (toBeRemoved != null) {
			for (final Issue i : issues) {
				if (!toBeRemoved.contains(i)) {
					res.addIssue(i);
				}
			}
		}
		return res;
	}

	public FluentIssueCollection errorsOnly() {
		final Severity severity = Severity.ERROR;

		return withSeverity(severity);
	}

	public FluentIssueCollection warningsOnly() {
		final Severity severity = Severity.WARNING;

		return withSeverity(severity);
	}

	public FluentIssueCollection withSeverity(final Severity... severities) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		for (final Issue i : issues) {
			if (Iterables.contains(Arrays.asList(severities), i.getSeverity())) {
				res.addIssue(i);
			}
		}
		return res;
	}

	public FluentIssueCollection named(final String expectedName) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		for (final Issue i : issues) {
			final EObject eObject = getEObject(i, resource);
			final String name = name(eObject);
			if (name.contains(expectedName)) {
				res.addIssue(i);
			}
		}
		if (res.getIssueCount() == 0) {
			res.addMessage("no elements found with name " + expectedName);
		}
		return res;
	}

	public FluentIssueCollection forElement(final Class<? extends EObject> cls, final String name) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		for (final Issue i : issues) {
			final EObject eObject = getEObject(i, resource);
			if (cls.isInstance(eObject)) {
				if (name.equalsIgnoreCase(name(eObject))) {
					res.addIssue(i);
				}
			}
		}
		if (res.getIssueCount() == 0) {
			res.addMessage("no elements of type " + cls.getName() + " named '" + name + "' found");
		}
		return res;
	}

	private int getIssueCount() {
		return issues.size();
	}

	public FluentIssueCollection under(final Class<? extends EObject> cls) {
		return under(cls, null);
	}

	public FluentIssueCollection under(final Class<? extends EObject> cls, final String name) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		for (final Issue i : issues) {
			final URI uri = i.getUriToProblem();
			final EObject eObject = resource.getEObject(uri.fragment());
			final EObject p = ancestor(eObject, cls);
			if (p != null) {
				if (name != null) {
					if (name.equals(name(p))) {
						res.addIssue(i);
					}
				} else {
					res.addIssue(i);
				}
			}
		}
		if (res.getIssueCount() == 0) {
			res.addMessage("did not find issues under a " + cls.getName() + " named '" + name + "'");
		}
		return res;
	}

	public FluentIssueCollection sizeIs(final int i) {
		if (issues.size() == i) {
			state = true;
		} else {
			addMessage("failed size: expected " + i + ", actual " + issues.size());
			state = false;
		}
		return this;
	}

	public FluentIssueCollection oneOfThemContains(final String substring) {
		boolean found = false;
		for (final Issue i : issues) {
			if (i.getMessage().toLowerCase().contains(substring.toLowerCase())) {
				found = true;
			}
		}
		if (found) {
			reportOk();
		} else {
			addMessage("failed oneOfThemContains: none of the issues contains substring '" + substring + "'");
			reportError();
		}
		return this;
	}

	public FluentIssueCollection nOfThemContain(final int n, final String substring) {
		int count = 0;
		for (final Issue i : issues) {
			if (i.getMessage().toLowerCase().contains(substring.toLowerCase())) {
				count++;
			}
		}
		if (count == n) {
			reportOk();
		} else {
			addMessage("failed nOfThemContain: expected " + n + " with substring '" + substring + "', but '" + count
					+ "' found.");
			reportError();
		}
		return this;
	}

	public FluentIssueCollection allOfThemContain(final String substring) {
		for (final Issue i : issues) {
			if (!i.getMessage().toLowerCase().contains(substring.toLowerCase())) {
				reportError();
				addMessage("failed allOfThemContain: not all issues contain the substring '" + substring + "'");
			}
		}
		reportOk();
		return this;
	}

	public FluentIssueCollection theOneAndOnlyContains(final String substring) {
		if (issues.size() > 1) {
			reportError();
			addMessage("failed theOneAndOnlyContains: expecting a single issue (theSingleOneReads) but found: "
					+ issues.size());
			for (final Issue issue : issues) {
				LOGGER.debug("  line " + issue.getLineNumber() + ": " + issue.getMessage() + " / "
						+ issue.getUriToProblem());
			}
			return this;
		}
		return oneOfThemContains(substring);
	}

	/**
	 * Filters all issues with a specific {@link Issue#getCode() issue code}.
	 * 
	 * @param code
	 *            Issue code
	 * @return A new instance containing the issues with the given code.
	 */
	public FluentIssueCollection withCode(final String code) {
		final FluentIssueCollection res = new FluentIssueCollection(resource, messages);
		for (final Issue i : issues) {
			if (i.getCode().equals(code)) {
				res.addIssue(i);
			}
		}
		if (res.getIssueCount() == 0) {
			res.addMessage("failed withCode: no issues found with code '" + code + "'");
		}
		return res;
	}

	public boolean evaluate() {
		return state;
	}

	protected void reportOk() {
		if (stateIsSet) {
			if (state) {
				state = true;
			}
			if (!state) {
				state = false;
			}
		} else {
			state = true;
		}
		stateIsSet = true;
	}

	protected void reportError() {
		if (stateIsSet) {
			if (state) {
				state = false;
			}
			if (!state) {
				state = false;
			}
		} else {
			state = false;
		}
		stateIsSet = true;
	}

	public List<Issue> getIssues() {
		return issues;
	}

	public Resource getResource() {
		return resource;
	}

	public List<String> getMessages() {
		return messages;
	}

	public String getMessageString() {
		final StringBuffer sb = new StringBuffer();
		for (final String m : messages) {
			sb.append("\n  - " + m);
		}
		return sb.toString();
	}

	public void dumpIssues() {
		LOGGER.debug("--- Issues ---");
		for (final Issue i : issues) {
			dumpIssue(resource, i);
		}
	}

	public String getSummary() {
		if (issues.size() == 0) {
			return "No issues";
		}

		final StringBuffer sb = new StringBuffer();
		sb.append("Issues:");
		for (final Issue i : issues) {
			sb.append("\n  - " + getIssueSummary(resource, i));
		}
		return sb.toString();
	}

	public static void dumpIssue(final Resource resource, final Issue issue) {
		LOGGER.debug(getIssueSummary(resource, issue));
	}

	public static String getIssueSummary(final Resource resource, final Issue issue) {
		boolean validFragment = true;
		if (issue.getUriToProblem() == null || "//".equals(issue.getUriToProblem().fragment())) {
			validFragment = false;
		}

		if (validFragment) {
			final EObject eObject = resource.getEObject(issue.getUriToProblem().fragment());
			final EClass cls = eObject.eClass();
			return issue.getSeverity() + " at " + cls.getName() + "( line " + issue.getLineNumber() + "): "
					+ issue.getMessage();
		} else {
			return issue.getSeverity() + "( line " + issue.getLineNumber() + "): " + issue.getMessage();
		}
	}

	@Override
	public Iterator<Issue> iterator() {
		return issues.iterator();
	}

}