blob: 119650e122b722f81248fc1b8a8afbdd8c1878bc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2022 Willink Transformations and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* E.D.Willink - Initial API and implementation
*******************************************************************************/
package org.eclipse.ocl.pivot.internal.resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.NotificationChain;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.xmi.XMLSave;
import org.eclipse.emf.ecore.xmi.impl.XMIHelperImpl;
import org.eclipse.emf.ecore.xmi.impl.XMIResourceImpl;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.ocl.pivot.Constraint;
import org.eclipse.ocl.pivot.Element;
import org.eclipse.ocl.pivot.ExpressionInOCL;
import org.eclipse.ocl.pivot.Feature;
import org.eclipse.ocl.pivot.InvalidType;
import org.eclipse.ocl.pivot.Model;
import org.eclipse.ocl.pivot.PivotPackage;
import org.eclipse.ocl.pivot.Property;
import org.eclipse.ocl.pivot.internal.utilities.PivotObjectImpl;
import org.eclipse.ocl.pivot.internal.utilities.PivotUtilInternal;
import org.eclipse.ocl.pivot.resource.ASResource;
import org.eclipse.ocl.pivot.util.PivotPlugin;
import org.eclipse.ocl.pivot.utilities.PivotConstants;
import org.eclipse.ocl.pivot.utilities.TracingAdapter;
import org.eclipse.ocl.pivot.utilities.TracingOption;
import org.eclipse.ocl.pivot.utilities.TreeIterable;
import org.eclipse.ocl.pivot.utilities.XMIUtil;
/**
* ASResourceImpl is the mandatory implementation of the ASResource interface that refines an
* a standard EMF XMIResource to be used as a Pivot AS Resource.
* @author ed
*
*/
public class ASResourceImpl extends XMIResourceImpl implements ASResource
{
/**
* If CHECK_IMMUTABILITY is set active, an ImmutabilityCheckingAdapter instance is installed for all
* contents of any ASREsource that is set not-saveable. Any mutation then causes an IllegalStateException.
*
* @since 1.5
*/
public static final TracingOption CHECK_IMMUTABILITY = new TracingOption(PivotPlugin.PLUGIN_ID, "resource/checkImmutability"); //$NON-NLS-1$
/**
* QVTd JUnit tests may set this false to load the saved XMI as text for validation that it is free of
* references to undeclared xmi:ids. The OCL JUnit tests do not have sufficient referential complexity to have problems.
* See Bug 578030.
*
* @since 1.18
*/
public static boolean SKIP_CHECK_BAD_REFERENCES = false;
/**
* An adapter implementation for tracking resource modification. This is only in use if the
* "resource/checkImmutability" .option is set as is the case for JUnit tests.
*
* @since 1.18
*/
protected class ImmutabilityCheckingAdapter extends AdapterImpl
{
private @NonNull String formatMutationMessage(@NonNull Notification notification) {
StringBuilder s = new StringBuilder();
s.append("'");
s.append(ASResourceImpl.this.getURI());
s.append("' modified at a '");
s.append(TracingAdapter.getFeatureType(notification));
s.append("'");
return s.toString();
}
@Override
public void notifyChanged(Notification notification) { // FIXME All irregularitoes should be transient
if (!notification.isTouch() && !ASResourceImpl.this.isUpdating && !ASResourceImpl.this.isUnloading) {
Object notifier = notification.getNotifier();
Object feature = notification.getFeature();
int eventType = notification.getEventType();
if (eventType == Notification.ADD) {
if (notifier instanceof Resource) {
int featureID = notification.getFeatureID(Resource.class); // Occurs after unloading has finished
if (featureID == RESOURCE__ERRORS) {
return;
}
if (featureID == RESOURCE__WARNINGS) {
return;
}
}
else if (notifier instanceof Element) {
if (feature == PivotPackage.Literals.ELEMENT__OWNED_EXTENSIONS) {
return;
}
else if (notifier instanceof ExpressionInOCL) { // A known safe laziness See Bug 535888#c6 and Bug 551822#c2
if (feature == PivotPackage.Literals.EXPRESSION_IN_OCL__OWNED_PARAMETERS) {
return;
}
}
else if (notifier instanceof org.eclipse.ocl.pivot.Class) {
if (feature == PivotPackage.Literals.CLASS__OWNED_PROPERTIES) {
Object newValue = notification.getNewValue();
if ((newValue instanceof Property) && ((Property)newValue).isIsImplicit()) { // Late QVTr trace properties
return;
}
return;
}
else if (notifier instanceof InvalidType) { // A known safe laziness See Bug 579037#c2
// return;
}
}
}
}
else if (eventType == Notification.REMOVE) {
if (notifier instanceof org.eclipse.ocl.pivot.Class) {
if (feature == PivotPackage.Literals.CLASS__OWNED_PROPERTIES) {
Object oldValue = notification.getOldValue();
if ((oldValue instanceof Property) && ((Property)oldValue).isIsImplicit()) { // Late QVTr trace properties
return;
}
return;
}
}
}
else if (eventType == Notification.SET) {
if (notifier instanceof Resource) {
int featureID = notification.getFeatureID(Resource.class); // Occurs after unloading has finished
if (featureID == RESOURCE__IS_LOADED) {
return;
}
// if (featureID == RESOURCE__URI) {
// return;
// }
if (featureID == RESOURCE__TIME_STAMP) {
return;
}
// Resource resource = (Resource)notifier;
// if (!resource.isLoaded()) {
// return;
// }
}
else if (notifier instanceof Feature) {
if (feature == PivotPackage.Literals.FEATURE__IMPLEMENTATION) { // A known safe transient See Bug 535888#c6 and Bug 551822#c2
Object oldValue = notification.getOldValue();
if (oldValue == null) {
return;
}
}
if ((notifier instanceof Property) && ((Property)notifier).isIsImplicit()) { // Occurs when unloading QVTr trace properties
// return;
}
}
else if (notifier instanceof Constraint) {
if (feature == PivotPackage.Literals.CONSTRAINT__OWNED_SPECIFICATION) {
return;
}
}
else if (notifier instanceof ExpressionInOCL) { // A known safe laziness See Bug 535888#c6
if (feature == PivotPackage.Literals.EXPRESSION_IN_OCL__OWNED_BODY) {
return;
}
if (feature == PivotPackage.Literals.EXPRESSION_IN_OCL__OWNED_CONTEXT) {
return;
}
if (feature == PivotPackage.Literals.EXPRESSION_IN_OCL__OWNED_RESULT) {
return;
}
if (feature == PivotPackage.Literals.LANGUAGE_EXPRESSION__BODY) {
return;
}
if (feature == PivotPackage.Literals.LANGUAGE_EXPRESSION__OWNING_CONSTRAINT) {
return;
}
if (feature == PivotPackage.Literals.TYPED_ELEMENT__IS_REQUIRED) {
return;
}
if (feature == PivotPackage.Literals.TYPED_ELEMENT__TYPE) {
return;
}
}
}
// Drop through for nearly everything including REMOVE - see Bug 541380#c6
throw new IllegalStateException(formatMutationMessage(notification));
}
}
@Override
public void setTarget(Notifier newTarget) {}
@Override
public void unsetTarget(Notifier oldTarget) {}
}
/**
* ImmutableResource provides additional API for derived ReadOnly/Immutable implementations.
*
* @since 1.5
*/
public static interface ImmutableResource
{
/**
* Return true if this Immutable/ReadOnly Resource is compatible with the given metamodelURI.
* This is typically used to allow a metamodelURI implementation to be re-used rather than cloned.
*/
boolean isCompatibleWith(@NonNull String metamodelURI);
}
/**
* @since 1.5
*/
private @Nullable ImmutabilityCheckingAdapter immutabilityCheckingAdapter = null;
protected final @NonNull ASResourceFactory asResourceFactory;
private @Nullable LUSSIDs lussids = null;
private @Nullable Map<@NonNull String, @NonNull EObject> legacyXMIId2eObject = null;
/**
* An attempt to save an unsaveable ASResource is ignored, probably because it is immuatble..
*/
private boolean isSaveable = true;
/**
* Set true during doUnload()
*/
private boolean isUnloading = false;
/**
* Set true/false by setUpdating()(
*/
private boolean isUpdating = false;
/**
* Creates an instance of the resource.
*/
public ASResourceImpl(@NonNull URI uri, @NonNull ASResourceFactory asResourceFactory) {
super(uri);
this.asResourceFactory = asResourceFactory;
assert PivotUtilInternal.isASURI(uri);
// PivotUtilInternal.debugPrintln("Create " + NameUtil.debugSimpleName(this) + " : " + uri);
}
/**
* @since 1.4
*/
@Override
public @Nullable EObject basicGetEObjectByID(@Nullable String id) {
return idToEObjectMap != null ? idToEObjectMap.get(id) : null;
}
/**
* @since 1.4
*/
@Override
public @Nullable LUSSIDs basicGetLUSSIDs() {
return lussids;
}
/**
* Overridden to ensure that the ResourceFactoryRegistry ExtensionToFactoryMap entries for AS file extensions
* have ASResourceFactory instnaces that are able to fall back from AS extension to CS extension using the
* resourceSet as the AS ResourceSet for OCL parsing.
*/
@Override
public NotificationChain basicSetResourceSet(ResourceSet resourceSet, NotificationChain notifications) {
NotificationChain notificationChain = super.basicSetResourceSet(resourceSet, notifications);
if (resourceSet != null) {
String fileExtension = getURI().fileExtension();
if (fileExtension != null) {
Object resourceFactory = resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().get(fileExtension);
if (resourceFactory == null) {
ASResourceFactoryRegistry.INSTANCE.configureResourceFactoryRegistry(resourceSet);
}
}
}
return notificationChain;
}
/**
* @since 1.18
*/
protected @NonNull ImmutabilityCheckingAdapter createImmutabilityCheckingAdapter() {
return new ImmutabilityCheckingAdapter();
}
@Override
protected XMLSave createXMLSave() {
return new PivotSaveImpl(new XMIHelperImpl(this)
{
@Override
public String getHREF(EObject obj) {
if (obj instanceof Property) { // Avoid generating a referemce to an EObject that might not exist
Property asProperty = (Property)obj;
if (asProperty.isIsImplicit() && (asProperty.getOpposite() != null)) {
return null;
}
}
return super.getHREF(obj);
}
});
}
@Override
protected void doUnload() {
isUnloading = true;
try {
super.doUnload();
if (lussids != null) {
resetLUSSIDs();
}
}
finally {
isUnloading = false;
}
}
@Override
public @NonNull ASResourceFactory getASResourceFactory() {
return asResourceFactory;
}
@Override
public @NonNull Map<Object, Object> getDefaultSaveOptions() {
Map<Object, Object> defaultSaveOptions2 = defaultSaveOptions;
if (defaultSaveOptions2 == null) {
defaultSaveOptions = defaultSaveOptions2 = XMIUtil.createPivotSaveOptions();
}
return defaultSaveOptions2;
}
@SuppressWarnings("deprecation")
@Override
protected EObject getEObjectByID(String id) {
if ((unloadingContents == null) && (idToEObjectMap == null)) { // Lazy xmi:id creation needed by generated ASResources
AS2ID.assignIds(this, null);
}
if (idToEObjectMap == null) {
return null;
}
EObject eObject = idToEObjectMap.get(id);
if (eObject != null) {
return eObject;
}
if (isLoading()) {
return null;
}
// FIXME Use getXmiidVersion() to select appropriate algorithm
Map<@NonNull String, @NonNull EObject> legacyXMIId2eObject2 = legacyXMIId2eObject;
if (legacyXMIId2eObject2 == null) {
org.eclipse.ocl.pivot.internal.utilities.AS2XMIid as2id = new org.eclipse.ocl.pivot.internal.utilities.AS2XMIid();
legacyXMIId2eObject = legacyXMIId2eObject2 = new HashMap<>();
for (EObject eObject2 : new TreeIterable(this)) {
if (eObject2 instanceof Element) {
Element element = (Element)eObject2;
org.eclipse.ocl.pivot.utilities.AS2XMIidVisitor idVisitor = asResourceFactory.createAS2XMIidVisitor(as2id);
Boolean status = element.accept(idVisitor);
if (status == Boolean.TRUE) {
String legacyId = idVisitor.toString();
if (legacyId != null) {
legacyXMIId2eObject2.put(legacyId, eObject2);;
}
}
}
}
}
EObject eObject2 = legacyXMIId2eObject2.get(id);
return eObject2;
}
/**
* @since 1.4
*/
@Override
public @NonNull LUSSIDs getLUSSIDs(@NonNull Map<@NonNull Object, @Nullable Object> options) {
LUSSIDs lussids2 = lussids;
if (lussids2 == null) {
lussids = lussids2 = ((ASResourceFactory.ASResourceFactoryExtension)asResourceFactory).createLUSSIDs(this, options);
}
return lussids2;
}
@Override
public @NonNull Model getModel() {
EList<EObject> contents = getContents();
if (contents.size() <= 0) {
throw new IllegalStateException("No Model at root of empty '" + getURI() + "'");
}
EObject eObject = contents.get(0);
if (!(eObject instanceof Model)) {
throw new IllegalStateException("Non-Model at root of '" + getURI() + "'");
}
return (Model)eObject;
}
@Override
public String getURIFragment(EObject eObject) {
if ((unloadingContents == null) && (idToEObjectMap == null)) {
AS2ID.assignIds(this, null);
}
return super.getURIFragment(eObject);
}
/**
* @since 1.4
*/
@Override
public int getXmiidVersion() {
for (EObject eRoot : getContents()) {
if (eRoot instanceof Model) {
Number xmiidVersion = ((Model)eRoot).getXmiidVersion();
if (xmiidVersion != null) {
return xmiidVersion.intValue();
}
}
}
return 0;
}
/**
* Read the serialized representation to confirm that all external references
* use an xmi:id to enhance persistence in the case of model evolution.
*/
private boolean isFreeOfBadReferences() throws IOException {
InputStream inputStream = getResourceSet().getURIConverter().createInputStream(uri);
Reader reader = new InputStreamReader(inputStream);
StringBuilder s = new StringBuilder();
char[] buf = new char[4096];
for (int len; (len = reader.read(buf)) > 0; ) {
s.append(buf, 0, len);
}
reader.close();
String string = s.toString();
int index = string.indexOf(";#//@");
if (index < 0) {
return true;
}
int preIndex = string.lastIndexOf("\n", index);
int postIndex = string.indexOf("\n", index);
String refText = string.substring(preIndex, postIndex).trim();
System.err.println("Missing xmi:id for reference in \'" + uri + "'\n\t" + refText);
// PivotLUSSIDs.isExternallyReferenceable determines what gets xmi:ids
return false;
}
@Override
public boolean isOrphanage() {
return false;
}
@Override
public boolean isSaveable() {
return isSaveable;
}
/**
* @since 1.4
*/
@Override
public void resetLUSSIDs() {
LUSSIDs lussids2 = lussids;
if (lussids2 != null) {
lussids = null;
// System.out.println("resetLUSSIDs for " + getURI());
lussids2.dispose();
}
}
/**
* Overridden to suppress saving unsaveable content to a probably read-only destination.
*/
@Override
public void save(Map<?, ?> options) throws IOException {
if (isSaveable) {
setXmiidVersion(PivotConstants.XMIIDS_CURRENT);
if (options == null) {
options = getDefaultSaveOptions();
}
XMIUtil.IdResourceEntityHandler.reset(options);
super.save(options);
assert SKIP_CHECK_BAD_REFERENCES || isFreeOfBadReferences();
}
}
/**
* @since 1.5
*/
@Override
public boolean setSaveable(boolean isSaveable) {
boolean wasSaveable = this.isSaveable;
this.isSaveable = isSaveable;
if (isSaveable) {
if (immutabilityCheckingAdapter != null) {
this.eAdapters().remove(immutabilityCheckingAdapter);
for (TreeIterator<EObject> i = getAllProperContents(getContents()); i.hasNext(); ) {
EObject eObject = i.next();
eObject.eAdapters().remove(immutabilityCheckingAdapter);
}
immutabilityCheckingAdapter = null;
}
}
else if (wasSaveable && CHECK_IMMUTABILITY.isActive()) {
if (immutabilityCheckingAdapter == null) {
immutabilityCheckingAdapter = createImmutabilityCheckingAdapter();
}
for (TreeIterator<EObject> i = getAllProperContents(getContents()); i.hasNext(); ) {
EObject eObject = i.next();
eObject.eAdapters().add(immutabilityCheckingAdapter);
}
this.eAdapters().add(immutabilityCheckingAdapter);
}
return wasSaveable;
}
@Override
public boolean setUpdating(boolean isUpdating) {
boolean wasUpdating = this.isUpdating;
this.isUpdating = isUpdating;
return wasUpdating;
}
/**
* @since 1.4
*/
@Override
public void setXmiidVersion(int xmiidVersion) {
for (EObject eRoot : getContents()) {
if (eRoot instanceof Model) {
((Model)eRoot).setXmiidVersion(xmiidVersion);
}
}
}
/**
* @since 1.18
*/
protected String superGetURIFragment(EObject eObject) {
return super.getURIFragment(eObject); // Bypass assignIds for use by OrphanResource
}
@Override
protected void unloaded(InternalEObject internalEObject) {
if (internalEObject instanceof PivotObjectImpl) {
((PivotObjectImpl)internalEObject).unloaded(this);
}
super.unloaded(internalEObject);
}
@Override
protected boolean useIDAttributes() {
return false;
}
@Override
protected boolean useIDs() {
return true;
}
}