All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.xtext.linking.lazy.LazyLinkingResource Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2008 itemis AG (http://www.itemis.eu) and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 *******************************************************************************/
package org.eclipse.xtext.linking.lazy;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.impl.EClassImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.InternalEList;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.diagnostics.DiagnosticMessage;
import org.eclipse.xtext.diagnostics.ExceptionDiagnostic;
import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider;
import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext;
import org.eclipse.xtext.linking.ILinkingService;
import org.eclipse.xtext.linking.impl.IllegalNodeException;
import org.eclipse.xtext.linking.impl.LinkingHelper;
import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.util.CancelIndicator;
import org.eclipse.xtext.util.Triple;

import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Provider;

/**
 * @author Sven Efftinge - Initial contribution and API
 * @author Holger Schill
 */
public class LazyLinkingResource extends XtextResource {

	private static Logger log = Logger.getLogger(LazyLinkingResource.class);
	
	/**
	 * The cache key for a Set of uri fragments that cannot be resolved. 
	 * @since 2.4
	 */
	public static final String UNRESOLVEABLE_PROXIES_KEY = "UNRESOLVEABLE_PROXIES";

	@Inject
	private ILinkingService linkingService;

	@Inject
	private LazyURIEncoder encoder;

	@Inject
	private ILinkingDiagnosticMessageProvider diagnosticMessageProvider;
	
	@Inject
	private ILinkingDiagnosticMessageProvider.Extended linkingDiagnosticMessageProvider;

	@Inject
	private LinkingHelper linkingHelper;

	private boolean eagerLinking = false;

	@Override
	protected void doLoad(InputStream inputStream, Map options) throws IOException {
		super.doLoad(inputStream, options);
		if (options != null && Boolean.TRUE.equals(options.get(OPTION_RESOLVE_ALL)))
			EcoreUtil.resolveAll(this);
	}

	@Override
	protected void doLinking() {
		super.doLinking();
		if (isEagerLinking())
			EcoreUtil.resolveAll(this);
	}

	private LinkedHashSet> resolving = Sets.newLinkedHashSet();

	/**
	 * resolves any lazy cross references in this resource, adding Issues for unresolvable elements to this resource.
	 * This resource might still contain resolvable proxies after this method has been called.
	 * 
	 * @param mon a {@link CancelIndicator} can be used to stop the resolution.
	 */
	public void resolveLazyCrossReferences(final CancelIndicator mon) {
		final CancelIndicator monitor = mon == null ? CancelIndicator.NullImpl : mon;
		TreeIterator iterator = EcoreUtil.getAllContents(this, true);
		while (iterator.hasNext()) {
			if (monitor.isCanceled())
				return;
			InternalEObject source = (InternalEObject) iterator.next();
			EStructuralFeature[] eStructuralFeatures = ((EClassImpl.FeatureSubsetSupplier) source.eClass()
					.getEAllStructuralFeatures()).crossReferences();
			if (eStructuralFeatures != null) {
				for (EStructuralFeature crossRef : eStructuralFeatures) {
					if (monitor.isCanceled())
						return;
					resolveLazyCrossReference(source, crossRef);
				}
			}
		}
	}

	protected void resolveLazyCrossReference(InternalEObject source, EStructuralFeature crossRef) {
		if (crossRef.isDerived() 
				|| (crossRef instanceof EReference && !((EReference)crossRef).isResolveProxies())
				|| crossRef.isTransient())
			return;
		if (crossRef.isMany()) {
			@SuppressWarnings("unchecked")
			InternalEList list = (InternalEList) source.eGet(crossRef);
			for (int i = 0; i < list.size(); i++) {
				EObject proxy = list.basicGet(i);
				if (proxy.eIsProxy()) {
					URI proxyURI = ((InternalEObject) proxy).eProxyURI();
					if (getURI().equals(proxyURI.trimFragment())) {
						final String fragment = proxyURI.fragment();
						if (getEncoder().isCrossLinkFragment(this, fragment)) {
							EObject target = getEObject(fragment);
							if (target != null) {
								try {
									source.eSetDeliver(false);
									list.setUnique(i, target);
								} finally {
									source.eSetDeliver(true);
								}
							}
						}
					}
				}
			}
		} else {
			EObject proxy = (EObject) source.eGet(crossRef, false);
			if (proxy != null && proxy.eIsProxy()) {
				URI proxyURI = ((InternalEObject) proxy).eProxyURI();
				if (getURI().equals(proxyURI.trimFragment())) {
					final String fragment = proxyURI.fragment();
					if (getEncoder().isCrossLinkFragment(this, fragment)) {
						EObject target = getEObject(fragment);
						if (target != null) {
							try {
								source.eSetDeliver(false);
								source.eSet(crossRef, target);
							} finally {
								source.eSetDeliver(true);
							}
						}
					}
				}
			}
		}
	}

	@Override
	public synchronized EObject getEObject(String uriFragment) {
		try {
			if (getEncoder().isCrossLinkFragment(this, uriFragment)) {
				Triple triple = getEncoder().decode(this, uriFragment);
				return getEObject(uriFragment, triple);
			}
		} catch (RuntimeException e) {
			getErrors().add(new ExceptionDiagnostic(e));
			log.error("resolution of uriFragment '" + uriFragment + "' failed.", e);
			// wrapped because the javaDoc of this method states that WrappedExceptions are thrown
			// logged because EcoreUtil.resolve will ignore any exceptions.
			throw new WrappedException(e);
		}
		return super.getEObject(uriFragment);
	}

	/**
	 * @since 2.4
	 */
	protected EObject getEObject(String uriFragment, Triple triple) throws AssertionError {
		try {
			if (!resolving.add(triple))
				return handleCyclicResolution(triple);
			Set unresolveableProxies = getUnresolvableURIFragments();
			if (unresolveableProxies.contains(uriFragment))
				return null;
			EReference reference = triple.getSecond();
			List linkedObjects = getLinkingService().getLinkedObjects(
					triple.getFirst(), 
					reference,
					triple.getThird());

			if (linkedObjects.isEmpty()) {
				if (isUnresolveableProxyCacheable(triple))
					unresolveableProxies.add(uriFragment);
				createAndAddDiagnostic(triple);
				return null;
			}
			if (linkedObjects.size() > 1)
				throw new IllegalStateException("linkingService returned more than one object for fragment "
						+ uriFragment);
			EObject result = linkedObjects.get(0);
			if (!EcoreUtil2.isAssignableFrom(reference.getEReferenceType(), result.eClass())) {
				log.error("An element of type " + result.getClass().getName()
						+ " is not assignable to the reference " + reference.getEContainingClass().getName()
						+ "." + reference.getName());
				if (isUnresolveableProxyCacheable(triple))
					unresolveableProxies.add(uriFragment);
				createAndAddDiagnostic(triple);
				return null;
			}

			// remove previously added error markers, since everything should be fine now
			unresolveableProxies.remove(uriFragment);
			removeDiagnostic(triple);
			return result;
		} catch (IllegalNodeException ex) {
			createAndAddDiagnostic(triple, ex);
			return null;
		} finally {
			resolving.remove(triple);
		}
	}

	/**
	 * @since 2.1
	 */
	protected boolean isUnresolveableProxyCacheable(Triple triple) {
		return true;
	}

	protected EObject handleCyclicResolution(Triple triple) throws AssertionError {
		throw new AssertionError("Cyclic resolution of lazy links : " + getReferences(triple, resolving));
	}

	protected String getReferences(Triple triple,
			LinkedHashSet> resolving2) {
		StringBuffer buffer = new StringBuffer();
		boolean found = false;
		for (Triple triple2 : resolving2) {
			found = found || triple2.equals(triple);
			if (found)
				buffer.append(getQualifiedName(triple2.getSecond())).append("->");
		}
		buffer.append(getQualifiedName(triple.getSecond()));
		return buffer.toString();
	}

	private String getQualifiedName(EReference eReference) {
		return eReference.getEContainingClass().getName() + "." + eReference.getName();
	}

	protected static class DiagnosticMessageContext implements
			ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext {

		private final Triple triple;
		private final LinkingHelper linkingHelper;

		protected DiagnosticMessageContext(Triple triple, LinkingHelper helper) {
			this.triple = triple;
			this.linkingHelper = helper;
		}

		public EObject getContext() {
			return triple.getFirst();
		}

		public EReference getReference() {
			return triple.getSecond();
		}

		public String getLinkText() {
			return linkingHelper.getCrossRefNodeAsString(triple.getThird(), true);
		}

	}

	protected void createAndAddDiagnostic(Triple triple) {
		if (isValidationDisabled())
			return;
		DiagnosticMessage message = createDiagnosticMessage(triple);
		if (message != null) {
			List list = getDiagnosticList(message);
			Diagnostic diagnostic = createDiagnostic(triple, message);
			if (!list.contains(diagnostic))
				list.add(diagnostic);
		}
	}
	
	/**
	 * @since 2.3
	 */
	protected void createAndAddDiagnostic(Triple triple, IllegalNodeException ex) {
		if (isValidationDisabled())
			return;
		ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext context = createDiagnosticMessageContext(triple);
		DiagnosticMessage message = linkingDiagnosticMessageProvider.getIllegalNodeMessage(context, ex);
		if (message != null) {
			List list = getDiagnosticList(message);
			Diagnostic diagnostic = createDiagnostic(triple, message);
			if (!list.contains(diagnostic))
				list.add(diagnostic);
		}
	}

	protected void removeDiagnostic(Triple triple) {
		// return early if there's nothing to remove
		if (getErrors().isEmpty() && getWarnings().isEmpty())
			return;
		DiagnosticMessage message = createDiagnosticMessage(triple);
		List list = getDiagnosticList(message);
		if (!list.isEmpty()) {
			Diagnostic diagnostic = createDiagnostic(triple, message);
			list.remove(diagnostic);
		}
	}

	protected Diagnostic createDiagnostic(Triple triple, DiagnosticMessage message) {
		Diagnostic diagnostic = new XtextLinkingDiagnostic(triple.getThird(), message.getMessage(),
				message.getIssueCode(), message.getIssueData());
		return diagnostic;
	}

	protected List getDiagnosticList(DiagnosticMessage message) throws AssertionError {
		if (message != null) {
			switch (message.getSeverity()) {
				case ERROR:
					return getErrors();
				case WARNING:
					return getWarnings();
				default:
					throw new AssertionError("Unexpected severity: " + message.getSeverity());
			}
		}
		return Collections.emptyList();
	}

	protected DiagnosticMessage createDiagnosticMessage(Triple triple) {
		ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext context = createDiagnosticMessageContext(triple);
		DiagnosticMessage message = diagnosticMessageProvider.getUnresolvedProxyMessage(context);
		return message;
	}

	protected ILinkingDiagnosticContext createDiagnosticMessageContext(Triple triple) {
		return new DiagnosticMessageContext(triple, linkingHelper);
	}

	public void setLinkingService(ILinkingService linkingService) {
		this.linkingService = linkingService;
	}

	public ILinkingService getLinkingService() {
		return linkingService;
	}

	public void setEncoder(LazyURIEncoder encoder) {
		this.encoder = encoder;
	}

	public LazyURIEncoder getEncoder() {
		return encoder;
	}

	public void setEagerLinking(boolean eagerLinking) {
		this.eagerLinking = eagerLinking;
	}

	public boolean isEagerLinking() {
		return eagerLinking;
	}

	public ILinkingDiagnosticMessageProvider getDiagnosticMessageProvider() {
		return diagnosticMessageProvider;
	}

	public void setDiagnosticMessageProvider(ILinkingDiagnosticMessageProvider diagnosticMessageProvider) {
		this.diagnosticMessageProvider = diagnosticMessageProvider;
	}

	public LinkingHelper getLinkingHelper() {
		return linkingHelper;
	}

	public void setLinkingHelper(LinkingHelper linkingHelper) {
		this.linkingHelper = linkingHelper;
	}

	/**
	 * Marks the given proxy as unresolvable. Further attempts to resolve it by means of {@link #getEObject(String)}
	 * will yield null.
	 * @since 2.4
	 */
	public void markUnresolvable(EObject referenced) {
		if (!referenced.eIsProxy()) {
			throw new IllegalArgumentException("Cannot mark an instance as unresolvable if it is already resolved");
		}
		URI proxyURI = ((InternalEObject)referenced).eProxyURI();
		getUnresolvableURIFragments().add(proxyURI.fragment());
	}
	
	/**
	 * @since 2.4
	 */
	protected Set getUnresolvableURIFragments() {
		Set unresolveableProxies = getCache().get(UNRESOLVEABLE_PROXIES_KEY, this,
				new Provider>() {
					public Set get() {
						return Sets.newHashSet();
					}
				});
		return unresolveableProxies;
	}
}