diff --git a/pom.xml b/pom.xml index 1e6c199cc..19735bf00 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ views vsum + remote testutils applications @@ -144,6 +145,11 @@ guava 33.3.1-jre + + io.micrometer + micrometer-core + 1.13.3 + emf-compare org.eclipse.emf.compare @@ -255,6 +261,28 @@ 5.14.2 test + + + + org.eclipse.emfcloud + emfjson-jackson + 2.2.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.18.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.1 + + + com.fasterxml.jackson.core + jackson-core + 2.18.1 + diff --git a/remote/pom.xml b/remote/pom.xml new file mode 100644 index 000000000..dd3a05ed9 --- /dev/null +++ b/remote/pom.xml @@ -0,0 +1,164 @@ + + + + 4.0.0 + + + tools.vitruv + tools.vitruv.framework + 3.1.0-SNAPSHOT + + + tools.vitruv.framework.remote + + Vitruv V-SUM remote definition + + + + + + ${project.groupId} + tools.vitruv.change.utils + ${project.version} + compile + + + ${project.groupId} + tools.vitruv.change.composite + ${project.version} + + + ${project.groupId} + tools.vitruv.change.propagation + ${project.version} + + + ${project.groupId} + tools.vitruv.change.correspondence + ${project.version} + + + ${project.groupId} + tools.vitruv.change.interaction + ${project.version} + + + ${project.groupId} + tools.vitruv.change.atomic + ${project.version} + + + ${project.groupId} + tools.vitruv.framework.views + ${project.version} + + + ${project.groupId} + tools.vitruv.framework.vsum + ${project.version} + + + + + ${project.groupId} + tools.vitruv.change.testutils.metamodels + ${project.version} + test + + + ${project.groupId} + tools.vitruv.change.testutils.core + ${project.version} + test + + + + + com.google.guava + guava + + + org.eclipse.emf + org.eclipse.emf.common + + + org.eclipse.emf + org.eclipse.emf.ecore + + + org.eclipse.xtend + org.eclipse.xtend.lib + + + log4j + log4j + + + sdq-commons + edu.kit.ipd.sdq.commons.util.emf + + + org.eclipse.xtext + org.eclipse.xtext.xbase.lib + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + org.eclipse.emfcloud + emfjson-jackson + + + io.micrometer + micrometer-core + + + + + + org.hamcrest + hamcrest + test + + + xannotations + edu.kit.ipd.sdq.activextendannotations + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.platform + junit-platform-commons + test + + + org.junit.platform + junit-platform-launcher + test + + + sdq-commons + edu.kit.ipd.sdq.commons.util.java + test + + + \ No newline at end of file diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/VitruvClient.java b/remote/src/main/java/tools/vitruv/framework/remote/client/VitruvClient.java new file mode 100644 index 000000000..d76cd82e4 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/VitruvClient.java @@ -0,0 +1,11 @@ +package tools.vitruv.framework.remote.client; + +import tools.vitruv.framework.views.ViewProvider; +import tools.vitruv.framework.views.ViewTypeProvider; + +/** + * A Vitruvius client can remotely access the available {@link tools.vitruv.framework.views.ViewType}s of a Vitruvius instance and query + * {@link tools.vitruv.framework.views.ViewSelector}s in order to create remotely editable {@link tools.vitruv.framework.views.View}s. + */ +public interface VitruvClient extends ViewTypeProvider, ViewProvider { +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/VitruvClientFactory.java b/remote/src/main/java/tools/vitruv/framework/remote/client/VitruvClientFactory.java new file mode 100644 index 000000000..887538929 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/VitruvClientFactory.java @@ -0,0 +1,44 @@ +package tools.vitruv.framework.remote.client; + +import tools.vitruv.framework.remote.client.impl.VitruvRemoteConnection; +import tools.vitruv.framework.remote.common.DefaultConnectionSettings; + +import java.nio.file.Path; + +public class VitruvClientFactory { + /** + * Creates a new {@link VitruvClient} using the given host name or IP address and the standard port of 8080. + * + * @param url The host name or IP address of the Vitruvius server. + * @param temp A non-existing or empty directory for temporary files. + * @return A {@link VitruvClient}. + */ + public static VitruvClient create(String url, Path temp) { + return create(url, DefaultConnectionSettings.STD_PORT, temp); + } + + /** + * Creates a new {@link VitruvClient} using the given host name or IP address and port. + * + * @param hostOrIp The host name or IP address of the Vitruvius server. + * @param port Port of the Vitruvius server. + * @param temp A non-existing or empty directory for temporary files. + * @return A {@link VitruvClient}. + */ + public static VitruvClient create(String hostOrIp, int port, Path temp) { + return create(DefaultConnectionSettings.STD_PROTOCOL, hostOrIp, port, temp); + } + + /** + * Creates a new {@link VitruvClient} using the given protocol, host name or IP address, and port. + * + * @param protocol The protocol. + * @param hostOrIp The host name of IP address of the Vitruvius server. + * @param port Port of the Vitruvius server. + * @param temp A non-existing or empty directory for temporary files. + * @return A {@link VitruvClient}. + */ + public static VitruvClient create(String protocol, String hostOrIp, int port, Path temp) { + return new VitruvRemoteConnection(protocol, hostOrIp, port, temp); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/exception/BadClientResponseException.java b/remote/src/main/java/tools/vitruv/framework/remote/client/exception/BadClientResponseException.java new file mode 100644 index 000000000..50f896fb0 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/exception/BadClientResponseException.java @@ -0,0 +1,19 @@ +package tools.vitruv.framework.remote.client.exception; + +public class BadClientResponseException extends RuntimeException { + public BadClientResponseException() { + super(); + } + + public BadClientResponseException(String msg) { + super(msg); + } + + public BadClientResponseException(String msg, Throwable cause) { + super(msg, cause); + } + + public BadClientResponseException(Throwable cause) { + super(cause); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/exception/BadServerResponseException.java b/remote/src/main/java/tools/vitruv/framework/remote/client/exception/BadServerResponseException.java new file mode 100644 index 000000000..66c0346f1 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/exception/BadServerResponseException.java @@ -0,0 +1,30 @@ +package tools.vitruv.framework.remote.client.exception; + +public class BadServerResponseException extends RuntimeException { + private int statusCode = -1; + + public BadServerResponseException() { + super(); + } + + public BadServerResponseException(String msg) { + super(msg); + } + + public BadServerResponseException(String msg, int statusCode) { + super(msg); + this.statusCode = statusCode; + } + + public BadServerResponseException(String msg, Throwable cause) { + super(msg, cause); + } + + public BadServerResponseException(Throwable cause) { + super(cause); + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/exception/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/client/exception/package-info.java new file mode 100644 index 000000000..9cf0f714c --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/exception/package-info.java @@ -0,0 +1,4 @@ +/** + * This package defines exceptions for the Vitruvius client. + */ +package tools.vitruv.framework.remote.client.exception; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/impl/ChangeDerivingRemoteView.java b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/ChangeDerivingRemoteView.java new file mode 100644 index 000000000..ca7482e84 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/ChangeDerivingRemoteView.java @@ -0,0 +1,144 @@ +package tools.vitruv.framework.remote.client.impl; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; + +import edu.kit.ipd.sdq.commons.util.org.eclipse.emf.ecore.resource.ResourceCopier; +import edu.kit.ipd.sdq.commons.util.org.eclipse.emf.ecore.resource.ResourceSetUtil; +import tools.vitruv.change.atomic.hid.HierarchicalId; +import tools.vitruv.change.composite.description.VitruviusChange; +import tools.vitruv.change.composite.description.VitruviusChangeFactory; +import tools.vitruv.framework.views.CommittableView; +import tools.vitruv.framework.views.ViewSelection; +import tools.vitruv.framework.views.ViewSelector; +import tools.vitruv.framework.views.ViewType; +import tools.vitruv.framework.views.changederivation.StateBasedChangeResolutionStrategy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +/** + * A {@link RemoteView} that derives changes based on the changed state of its resources and allows to propagate them + * back to the Vitruvius server using the {@link #commitChanges} method. + */ +public class ChangeDerivingRemoteView implements CommittableView { + private final RemoteView base; + private final StateBasedChangeResolutionStrategy resolutionStrategy; + + private Map originalResourceMapping; + + ChangeDerivingRemoteView(RemoteView base, StateBasedChangeResolutionStrategy resolutionStrategy) { + checkArgument(base != null, "base must not be null"); + checkState(!base.isModified(), "view must not be modified"); + checkState(!base.isOutdated(), "view must not be outdated"); + checkArgument(resolutionStrategy != null, "resolution strategy must not be null"); + this.base = base; + this.resolutionStrategy = resolutionStrategy; + + initializeResourceMapping(base.viewSource); + } + + private void initializeResourceMapping(ResourceSet source) { + originalResourceMapping = ResourceCopier.copyViewResources(source.getResources(), + ResourceSetUtil.withGlobalFactories(new ResourceSetImpl())); + } + + @Override + public void close() { + base.close(); + } + + @Override + public Collection getRootObjects() { + return base.getRootObjects(); + } + + @Override + public boolean isModified() { + return base.isModified(); + } + + @Override + public boolean isOutdated() { + return base.isOutdated(); + } + + @Override + public void update() { + base.update(); + initializeResourceMapping(base.viewSource); + } + + @Override + public boolean isClosed() { + return base.isClosed(); + } + + @Override + public void registerRoot(EObject object, URI persistAt) { + base.registerRoot(object, persistAt); + } + + @Override + public void moveRoot(EObject object, URI newLocation) { + base.moveRoot(object, newLocation); + } + + @Override + public ViewSelection getSelection() { + return base.getSelection(); + } + + @Override + public ViewType getViewType() { + return base.getViewType(); + } + + @Override + public CommittableView withChangeRecordingTrait() { + return base.withChangeRecordingTrait(); + } + + @Override + public CommittableView withChangeDerivingTrait(StateBasedChangeResolutionStrategy changeResolutionStrategy) { + return base.withChangeDerivingTrait(changeResolutionStrategy); + } + + /** + * Commits the changes made to the view and its containing elements. + * + * @throws IllegalStateException if called on a closed view + * @see #isClosed() + * @see #commitChangesAndUpdate() + */ + @Override + public void commitChanges() { + base.checkNotClosed(); + var allChanges = new LinkedList>(); + base.viewSource.getResources().forEach(it -> { + var changes = findChanges(originalResourceMapping.get(it), it); + if (changes.getEChanges().size() > 0) { + allChanges.add(changes); + } + }); + base.remoteConnection.propagateChanges(base.uuid, VitruviusChangeFactory.getInstance().createCompositeChange(allChanges)); + base.modified = false; + } + + private VitruviusChange findChanges(Resource oldState, Resource newState) { + if (oldState == null) { + return resolutionStrategy.getChangeSequenceForCreated(newState); + } else if (newState == null) { + return resolutionStrategy.getChangeSequenceForDeleted(oldState); + } else { + return resolutionStrategy.getChangeSequenceBetween(newState, oldState); + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/impl/ChangeRecordingRemoteView.java b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/ChangeRecordingRemoteView.java new file mode 100644 index 000000000..52d7eb599 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/ChangeRecordingRemoteView.java @@ -0,0 +1,141 @@ +package tools.vitruv.framework.remote.client.impl; + +import java.util.Collection; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; + +import tools.vitruv.change.composite.description.TransactionalChange; +import tools.vitruv.change.composite.description.VitruviusChangeResolver; +import tools.vitruv.change.composite.recording.ChangeRecorder; +import tools.vitruv.change.interaction.UserInteractionBase; +import tools.vitruv.framework.views.CommittableView; +import tools.vitruv.framework.views.ViewSelection; +import tools.vitruv.framework.views.ViewSelector; +import tools.vitruv.framework.views.ViewType; +import tools.vitruv.framework.views.changederivation.StateBasedChangeResolutionStrategy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +/** + * A {@link RemoteView} that records changes to its resources and allows to propagate them + * back to the Vitruvius server using the {@link #commitChanges} method. + */ +public class ChangeRecordingRemoteView implements CommittableView { + private final RemoteView base; + private ChangeRecorder changeRecorder; + + public ChangeRecordingRemoteView(RemoteView base) { + checkArgument(base != null, "base must not be null"); + checkState(!base.isModified(), "view must not be modified"); + this.base = base; + setupChangeRecorder(); + } + + private void setupChangeRecorder() { + changeRecorder = new ChangeRecorder(base.viewSource); + changeRecorder.addToRecording(base.viewSource); + changeRecorder.beginRecording(); + } + + @Override + public void close() { + if (!isClosed()) { + changeRecorder.close(); + } + base.close(); + } + + @Override + public Collection getRootObjects() { + return base.getRootObjects(); + } + + @Override + public boolean isModified() { + return base.isModified(); + } + + @Override + public boolean isOutdated() { + return base.isOutdated(); + } + + @Override + public void update() { + if (changeRecorder.isRecording()) { + changeRecorder.endRecording(); + } + changeRecorder.close(); + base.update(); + setupChangeRecorder(); + } + + @Override + public boolean isClosed() { + return base.isClosed(); + } + + @Override + public void registerRoot(EObject object, URI persistAt) { + base.registerRoot(object, persistAt); + } + + @Override + public void moveRoot(EObject object, URI newLocation) { + base.moveRoot(object, newLocation); + } + + @Override + public ViewSelection getSelection() { + return base.getSelection(); + } + + @Override + public ViewType getViewType() { + return base.getViewType(); + } + + + @Override + public CommittableView withChangeRecordingTrait() { + changeRecorder.close(); + return base.withChangeDerivingTrait(); + } + + @Override + public CommittableView withChangeDerivingTrait(StateBasedChangeResolutionStrategy changeResolutionStrategy) { + changeRecorder.close(); + return base.withChangeDerivingTrait(changeResolutionStrategy); + } + + /** + * Commits the changes made to the view and its containing elements. + * + * @throws IllegalStateException if called on a closed view + * @see #isClosed() + * @see #commitChangesAndUpdate() + */ + @Override + public void commitChanges() { + base.checkNotClosed(); + var recordedChange = changeRecorder.endRecording(); + var changeResolver = VitruviusChangeResolver.forHierarchicalIds(base.viewSource); + var unresolvedChanges = changeResolver.assignIds(recordedChange); + base.remoteConnection.propagateChanges(base.uuid, unresolvedChanges); + base.modified = false; + changeRecorder.beginRecording(); + } + + public void commitChanges(Iterable userInputs) { + base.checkNotClosed(); + var recordedChange = changeRecorder.endRecording(); + var changeResolver = VitruviusChangeResolver.forHierarchicalIds(base.viewSource); + var unresolvedChanges = changeResolver.assignIds(recordedChange); + ((TransactionalChange) unresolvedChanges).setUserInteractions(userInputs); + base.remoteConnection.propagateChanges(base.uuid, unresolvedChanges); + base.modified = false; + changeRecorder.beginRecording(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteView.java b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteView.java new file mode 100644 index 000000000..3347d4973 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteView.java @@ -0,0 +1,216 @@ +package tools.vitruv.framework.remote.client.impl; + +import java.util.Collection; + +import org.eclipse.emf.common.notify.Notification; +import org.eclipse.emf.common.notify.Notifier; +import org.eclipse.emf.common.notify.impl.AdapterImpl; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; + +import tools.vitruv.framework.views.CommittableView; +import tools.vitruv.framework.views.View; +import tools.vitruv.framework.views.ViewSelection; +import tools.vitruv.framework.views.ViewSelector; +import tools.vitruv.framework.views.ViewType; +import tools.vitruv.framework.views.changederivation.StateBasedChangeResolutionStrategy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +/** + * A {@link View} which is a copy of a {@link View} from the VSUM of a Vitruvius server. + *

+ * Actions performed on this remote view or to the original view can be synchronized via the network. This view uses + * a {@link VitruvRemoteConnection} to do so. + */ +public class RemoteView implements View { + private final ViewSelector selector; + + protected final String uuid; + protected final VitruvRemoteConnection remoteConnection; + + protected ResourceSet viewSource; + protected boolean modified = false; + + RemoteView(String uuid, ResourceSet viewSource, ViewSelector selector, VitruvRemoteConnection remoteConnection) { + checkArgument(uuid != null, "uuid must not be null"); + checkArgument(viewSource != null, "view source must not be null"); + checkArgument(remoteConnection != null, "remote connection must not be null"); + checkArgument(selector != null, "selector must not be null"); + this.uuid = uuid; + this.remoteConnection = remoteConnection; + this.viewSource = viewSource; + this.selector = selector; + + addChangeListeners(viewSource); + } + + /** + * Provides the root model elements of this view. + * + * @throws IllegalStateException If called on a closed view. + * @see View#isClosed() + */ + @Override + public Collection getRootObjects() { + checkNotClosed(); + return viewSource.getResources().stream().map(Resource::getContents).flatMap(Collection::stream).toList(); + } + + /** + * Checks whether the view was closed. Closed views cannot be used further. All + * methods may throw an {@link IllegalStateException}. + */ + @Override + public boolean isClosed() { + return remoteConnection.isViewClosed(uuid); + } + + /** + * Returns whether the view was modified. + */ + @Override + public boolean isModified() { + return modified; + } + + /** + * Returns whether the view is outdated, i.e., whether the underlying view + * sources have changed. + */ + @Override + public boolean isOutdated() { + return remoteConnection.isViewOutdated(uuid); + } + + /** + * Updates the view via the {@link VitruvRemoteConnection}, thus invalidating its previous state and now providing + * an updated view. This can only be done for an unmodified view. + * + * @throws UnsupportedOperationException If called on a modified view. + * @throws IllegalStateException If called on a closed view. + * @see #isClosed() + * @see #isModified() + */ + @Override + public void update() { + checkNotClosed(); + checkState(!isModified(), "cannot update from model when view is modified"); + removeChangeListeners(viewSource); + viewSource = remoteConnection.updateView(uuid); + modified = false; + addChangeListeners(viewSource); + } + + @Override + public void close() { + if (!isClosed()) { + remoteConnection.closeView(uuid); + viewSource.getResources().forEach(Resource::unload); + viewSource.getResources().clear(); + removeChangeListeners(viewSource); + } + } + + /** + * Persists the given object at the given {@link URI} and adds it as view root. + */ + @Override + public void registerRoot(EObject object, URI persistAt) { + checkNotClosed(); + checkArgument(object != null, "object must not be null"); + checkArgument(persistAt != null, "URI for root must not be null"); + viewSource.createResource(persistAt).getContents().add(object); + } + + /** + * Moves the given object to the given {@link URI}. The given {@link EObject} + * must already be a root object of the view, otherwise an + * {@link IllegalStateException} is thrown. + */ + @Override + public void moveRoot(EObject object, URI newLocation) { + checkNotClosed(); + checkArgument(object != null, "object to move must not be null"); + checkState(getRootObjects().contains(object), "view must contain element %s to move", object); + checkArgument(newLocation != null, "URI for new location of root must not be null"); + viewSource.getResources().stream().filter(it -> it.getContents().contains(object)).findFirst().get().setURI(newLocation); + } + + /** + * Returns the {@link ViewSelection} with which this view has been created. + */ + @Override + public ViewSelection getSelection() { + return selector; + } + + /** + * UNSUPPORTED AT THE MOMENT!! + */ + @Override + public ViewType getViewType() { + //The client has no knowledge which view type was used to create the remote view. + //Additionally, the client is not able to create views. + throw new UnsupportedOperationException(); + } + + /** + * Returns a {@link CommittableView} based on the view's configuration. + * Changes to commit are identified by recording any changes made to the view. + * + * @throws UnsupportedOperationException If called on a modified view. + * @throws IllegalStateException If called on a closed view. + * @see #isClosed() + * @see #isModified() + */ + @Override + public CommittableView withChangeRecordingTrait() { + checkNotClosed(); + return new ChangeRecordingRemoteView(this); + } + + /** + * Returns a {@link CommittableView} based on the view's configuration. + * Changes to commit are identified by comparing the current view state with its state from the last update. + * + * @param changeResolutionStrategy The change resolution strategy to use for view state comparison. Must not be null. + * @throws UnsupportedOperationException If called on a modified view. + * @throws IllegalStateException If called on a closed view. + * @see #isClosed() + * @see #isModified() + */ + @Override + public CommittableView withChangeDerivingTrait(StateBasedChangeResolutionStrategy changeResolutionStrategy) { + checkNotClosed(); + return new ChangeDerivingRemoteView(this, changeResolutionStrategy); + } + + void checkNotClosed() { + checkState(!isClosed(), "view is already closed"); + } + + private void addChangeListeners(Notifier notifier) { + notifier.eAdapters().add(new AdapterImpl() { + @Override + public void notifyChanged(Notification message) { + modified = true; + } + }); + + if (notifier instanceof ResourceSet resourceSet) { + resourceSet.getResources().forEach(this::addChangeListeners); + } else if (notifier instanceof Resource resource) { + resource.getContents().forEach(this::addChangeListeners); + } else if (notifier instanceof EObject eObject) { + eObject.eContents().forEach(this::addChangeListeners); + } + } + + private void removeChangeListeners(ResourceSet resourceSet) { + resourceSet.getAllContents().forEachRemaining(it -> it.eAdapters().clear()); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteViewSelector.java b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteViewSelector.java new file mode 100644 index 000000000..502f43398 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteViewSelector.java @@ -0,0 +1,93 @@ +package tools.vitruv.framework.remote.client.impl; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.emfcloud.jackson.resource.JsonResource; + +import tools.vitruv.framework.views.ModifiableViewSelection; +import tools.vitruv.framework.views.View; +import tools.vitruv.framework.views.ViewSelection; +import tools.vitruv.framework.views.ViewSelector; +import tools.vitruv.framework.views.selection.ElementViewSelection; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * A selector for selecting the elements to be represented in a view, but on the Vitruvius client itself. + * It is capable of acting as a builder for a view by providing an appropriate creation method, handling the remote connection. + */ +public class RemoteViewSelector implements ViewSelector { + private final String uuid; + private final VitruvRemoteConnection remoteConnection; + private final ModifiableViewSelection viewSelection; + + public RemoteViewSelector(String uuid, Resource selection, VitruvRemoteConnection remoteConnection) { + this.uuid = uuid; + this.remoteConnection = remoteConnection; + this.viewSelection = new ElementViewSelection(selection.getContents()); + } + + /** + * Creates a view by delegating the request to the Vitruvius server, performing the selection done by this selector. + * + * @return The created view. + */ + @Override + public View createView() { + return remoteConnection.getView(this); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public ViewSelection getSelection() { + return viewSelection; + } + + @Override + public Collection getSelectableElements() { + return viewSelection.getSelectableElements(); + } + + @Override + public boolean isSelected(EObject eObject) { + return viewSelection.isSelected(eObject); + } + + @Override + public boolean isSelectable(EObject eObject) { + return viewSelection.isSelectable(eObject); + } + + @Override + public void setSelected(EObject eObject, boolean selected) { + viewSelection.setSelected(eObject, selected); + } + + @Override + public boolean isViewObjectSelected(EObject eObject) { + return viewSelection.getSelectableElements().stream().anyMatch(it -> + EcoreUtil.equals(eObject, it) && viewSelection.isViewObjectSelected(it)); + } + + String getUUID() { + return this.uuid; + } + + List getSelectionIds() { + var ids = new LinkedList(); + viewSelection.getSelectableElements().forEach(it -> { + if (viewSelection.isSelected(it)) { + var resource = (JsonResource) it.eResource(); + ids.add(resource.getID(it)); + } + }); + return ids; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteViewType.java b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteViewType.java new file mode 100644 index 000000000..28bafa606 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/RemoteViewType.java @@ -0,0 +1,35 @@ +package tools.vitruv.framework.remote.client.impl; + +import tools.vitruv.framework.views.ChangeableViewSource; +import tools.vitruv.framework.views.ViewSelector; +import tools.vitruv.framework.views.ViewType; + +/** + * A Vitruvius view type representing actual types on the virtual model, but is still capable of providing a view selector and allows creating + * views by querying the Vitruvius server. + */ +public class RemoteViewType implements ViewType { + private final String name; + private final VitruvRemoteConnection remoteConnection; + + RemoteViewType(String name, VitruvRemoteConnection remoteConnection) { + this.name = name; + this.remoteConnection = remoteConnection; + } + + @Override + public String getName() { + return name; + } + + /** + * Returns the {@link ViewSelector} of the {@link ViewType}, which allows configuring views by delegating the request to the Vitruvius server. + * + * @param viewSource Ignored, can be null. + * @return A view selector for the view type represented by this remote view type. + */ + @Override + public ViewSelector createSelector(ChangeableViewSource viewSource) { + return remoteConnection.getSelector(name); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/impl/VitruvRemoteConnection.java b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/VitruvRemoteConnection.java new file mode 100644 index 000000000..707c818ec --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/VitruvRemoteConnection.java @@ -0,0 +1,273 @@ +package tools.vitruv.framework.remote.client.impl; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Objects; + +import org.eclipse.emf.ecore.resource.ResourceSet; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import tools.vitruv.change.atomic.root.InsertRootEObject; +import tools.vitruv.change.composite.description.VitruviusChange; +import tools.vitruv.change.utils.ProjectMarker; +import tools.vitruv.framework.remote.client.VitruvClient; +import tools.vitruv.framework.remote.client.exception.BadClientResponseException; +import tools.vitruv.framework.remote.client.exception.BadServerResponseException; +import tools.vitruv.framework.remote.common.json.JsonFieldName; +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.remote.common.rest.constants.EndpointPath; +import tools.vitruv.framework.remote.common.rest.constants.Header; +import tools.vitruv.framework.remote.common.util.ResourceUtil; +import tools.vitruv.framework.views.ViewSelector; +import tools.vitruv.framework.views.ViewType; + +/** + * A {@link VitruvRemoteConnection} acts as a {@link HttpClient} to forward requests to a Vitruvius server. + * This enables the ability to perform actions on this remote Vitruvius instance. + */ +public class VitruvRemoteConnection implements VitruvClient { + private static final String METRIC_CLIENT_NAME = "vitruv.client.rest.client"; + private final int port; + private final String hostOrIp; + private final String protocol; + private final HttpClient client; + private final JsonMapper mapper; + + /** + * Creates a new {@link VitruvRemoteConnection} using the given URL and port to connect to the Vitruvius server. + * + * @param protocol The protocol of the Vitruvius server. + * @param hostOrIp The host name of IP address of the Vitruvius server. + * @param port of the Vitruvius server. + */ + public VitruvRemoteConnection(String protocol, String hostOrIp, int port, Path temp) { + this.client = HttpClient.newHttpClient(); + this.protocol = protocol; + this.hostOrIp = hostOrIp; + this.port = port; + + try { + if (Files.notExists(temp) || (Files.isDirectory(temp) && Files.list(temp).findAny().isEmpty())) { + Files.createDirectories(temp); + ProjectMarker.markAsProjectRootFolder(temp); + } + } catch (IOException e) { + throw new IllegalArgumentException("Given temporary directory for models could not be created!", e); + } + + this.mapper = new JsonMapper(temp); + } + + /** + * Returns a list of remote representations of {@link ViewType}s available at the Vitruvius server. + */ + @Override + public Collection> getViewTypes() { + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.VIEW_TYPES)).GET().build(); + try { + var response = sendRequest(request); + var typeNames = mapper.deserializeArrayOf(response.body(), String.class); + var list = new LinkedList>(); + typeNames.forEach(it -> list.add(new RemoteViewType(it, this))); + return list; + } catch (IOException e) { + throw new BadClientResponseException(e); + } + } + + /** + * Returns a view selector for the given {@link ViewType} by querying the selector from the Vitruvius server. + * The view type must be of type {@link RemoteViewType} as these represent the actual view types available at the server side. + * + * @param viewType The {@link ViewType} to create a selector for. + * @return A {@link ViewSelector} for the given view type. + * @throws IllegalArgumentException If view type is no {@link RemoteViewType}. + */ + @Override + public S createSelector(ViewType viewType) { + if (!(viewType instanceof RemoteViewType)) { + throw new IllegalArgumentException("This vitruv client can only process RemoteViewType!"); + } + return viewType.createSelector(null); + } + + /** + * Queries the Vitruvius server to obtain a view selector from the view type with the given name. + * + * @param typeName The name of the view type. + * @return The selector generated with the view type of the given name. + * @throws BadServerResponseException If the server answered with a bad response or a connection error occurred. + */ + RemoteViewSelector getSelector(String typeName) throws BadServerResponseException { + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.VIEW_SELECTOR)) + .header(Header.VIEW_TYPE, typeName) + .GET() + .build(); + try { + var response = sendRequest(request); + var resource = mapper.deserializeResource(response.body(), JsonFieldName.TEMP_VALUE, ResourceUtil.createJsonResourceSet()); + return new RemoteViewSelector(response.headers().firstValue(Header.SELECTOR_UUID).get(), resource, this); + } catch (IOException e) { + throw new BadClientResponseException(e); + } + } + + /** + * Queries the Vitruvius server to obtain the view using the given view selector. + * + * @param selector The {@link tools.vitruv.framework.views.ViewSelector} which should be used to create the view. + * @return The view generated with the given view selector. + * @throws BadServerResponseException If the server answered with a bad response or a connection error occurred. + */ + RemoteView getView(RemoteViewSelector selector) throws BadServerResponseException { + try { + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.VIEW)) + .header(Header.SELECTOR_UUID, selector.getUUID()) + .POST(BodyPublishers.ofString(mapper.serialize(selector.getSelectionIds()))) + .build(); + var response = sendRequest(request); + var rSet = mapper.deserialize(response.body(), ResourceSet.class); + return new RemoteView(response.headers().firstValue(Header.VIEW_UUID).get(), + rSet, selector, this); + } catch (IOException e) { + throw new BadClientResponseException(e); + } + } + + /** + * Queries the Vitruvius server to propagate the given changes for the view with the given UUID. + * + * @param uuid UUID of the changed view. + * @param change The changes performed on the affected view. + * @throws BadServerResponseException If the server answered with a bad response or a connection error occurred. + */ + void propagateChanges(String uuid, VitruviusChange change) throws BadServerResponseException { + try { + change.getEChanges().forEach(it -> { + if (it instanceof InsertRootEObject) { + ((InsertRootEObject) it).setResource(null); + } + }); + var jsonBody = mapper.serialize(change); + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.VIEW)) + .header(Header.CONTENT_TYPE, ContentType.APPLICATION_JSON) + .header(Header.VIEW_UUID, uuid) + .method("PATCH", BodyPublishers.ofString(jsonBody)) + .build(); + sendRequest(request); + } catch (IOException e) { + throw new BadClientResponseException(e); + } + } + + /** + * Queries the Vitruvius server to close the view with the given. + * + * @param uuid UUID of the view. + * @throws BadServerResponseException If the server answered with a bad response or a connection error occurred. + */ + void closeView(String uuid) throws BadServerResponseException { + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.VIEW)) + .header(Header.VIEW_UUID, uuid) + .DELETE() + .build(); + sendRequest(request); + } + + /** + * Queries the Vitruvius serve to check if the view with the given ID is closed. + * + * @param uuid UUID of the view. + * @return {@code true} if the view is closed, {@code false} otherwise. + * @throws BadServerResponseException If the server answered with a bad response or a connection error occurred. + */ + boolean isViewClosed(String uuid) throws BadServerResponseException { + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.IS_VIEW_CLOSED)) + .header(Header.VIEW_UUID, uuid) + .GET() + .build(); + return sendRequestAndCheckBooleanResult(request); + } + + /** + * Queries the Vitruvius server to check if the view with the given ID is outdated. + * + * @param uuid UUID of the view. + * @return {@code true} if the view is outdated, {@code false} otherwise. + */ + boolean isViewOutdated(String uuid) { + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.IS_VIEW_OUTDATED)) + .header(Header.VIEW_UUID, uuid) + .GET() + .build(); + return sendRequestAndCheckBooleanResult(request); + } + + /** + * Queries the Vitruvius server to update the view with the given ID. + * + * @param uuid UUID of the view. + * @return The updated {@link ResourceSet} of the view. + * @throws BadServerResponseException If the server answered with a bad response or a connection error occurred. + */ + ResourceSet updateView(String uuid) throws BadServerResponseException { + var request = HttpRequest.newBuilder() + .uri(createURIFrom(EndpointPath.VIEW)) + .header(Header.VIEW_UUID, uuid) + .GET() + .build(); + try { + var response = sendRequest(request); + return mapper.deserialize(response.body(), ResourceSet.class); + } catch (IOException e) { + throw new BadClientResponseException(e); + } + } + + private boolean sendRequestAndCheckBooleanResult(HttpRequest request) { + var response = sendRequest(request); + if (!Objects.equals(response.body(), Boolean.TRUE.toString()) && !Objects.equals(response.body(), Boolean.FALSE.toString())) { + throw new BadServerResponseException("Expected response to be true or false! Actual: " + response); + } + return response.body().equals(Boolean.TRUE.toString()); + } + + private HttpResponse sendRequest(HttpRequest request) { + var timer = Timer.start(Metrics.globalRegistry); + try { + var response = client.send(request, BodyHandlers.ofString()); + if (response.statusCode() != HttpURLConnection.HTTP_OK) { + timer.stop(Metrics.timer(METRIC_CLIENT_NAME, "endpoint", request.uri().getPath(), "method", request.method(), "result", "" + response.statusCode())); + throw new BadServerResponseException(response.body(), response.statusCode()); + } + timer.stop(Metrics.timer(METRIC_CLIENT_NAME, "endpoint", request.uri().getPath(), "method", request.method(), "result", "success")); + return response; + } catch (IOException | InterruptedException e) { + timer.stop(Metrics.timer(METRIC_CLIENT_NAME, "endpoint", request.uri().getPath(), "method", request.method(), "result", "exception")); + throw new BadServerResponseException(e); + } + } + + private URI createURIFrom(String path) { + return URI.create(String.format("%s://%s:%d%s", protocol, hostOrIp, port, path)); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/impl/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/package-info.java new file mode 100644 index 000000000..9d2101207 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/impl/package-info.java @@ -0,0 +1,5 @@ +/** + * In this package, the Vitruvius View API is re-implemented for the Vitruvius client. + * Additionally, the package contains the remote connection handling. + */ +package tools.vitruv.framework.remote.client.impl; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/client/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/client/package-info.java new file mode 100644 index 000000000..7dba50332 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/client/package-info.java @@ -0,0 +1,4 @@ +/** + * This package provides the Vitruvius client. + */ +package tools.vitruv.framework.remote.client; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/DefaultConnectionSettings.java b/remote/src/main/java/tools/vitruv/framework/remote/common/DefaultConnectionSettings.java new file mode 100644 index 000000000..4ca62b067 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/DefaultConnectionSettings.java @@ -0,0 +1,13 @@ +package tools.vitruv.framework.remote.common; + +/** + * Defines default settings for the connection between a Vitruvius server and client. + * They are only used if no other settings are provided. + */ +public class DefaultConnectionSettings { + public static final String STD_PROTOCOL = "http"; + public static final String STD_HOST = "localhost"; + public static final int STD_PORT = 8080; + + private DefaultConnectionSettings() {} +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/apm/SingleMeasureRecordingTimer.java b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/SingleMeasureRecordingTimer.java new file mode 100644 index 000000000..8b7cce0d0 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/SingleMeasureRecordingTimer.java @@ -0,0 +1,38 @@ +package tools.vitruv.framework.remote.common.apm; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.pause.PauseDetector; +import io.micrometer.core.instrument.step.StepTimer; + +/** + * Provides a specialized {@link StepTimer}, which records every single measurement. + */ +public class SingleMeasureRecordingTimer extends StepTimer { + public static record SingleRecordedMeasure(long amount, TimeUnit unit) {} + + private List recordings = new ArrayList<>(); + + public SingleMeasureRecordingTimer(Id id, Clock clock, DistributionStatisticConfig distributionStatisticConfig, + PauseDetector pauseDetector, TimeUnit baseTimeUnit, long stepDurationMillis, boolean supportsAggregablePercentiles) { + super(id, clock, distributionStatisticConfig, pauseDetector, baseTimeUnit, stepDurationMillis, supportsAggregablePercentiles); + } + + @Override + protected void recordNonNegative(long amount, TimeUnit unit) { + super.recordNonNegative(amount, unit); + recordings.add(new SingleRecordedMeasure(amount, unit)); + } + + public List getRecordings() { + return List.copyOf(recordings); + } + + public void clear() { + recordings.clear(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvApmController.java b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvApmController.java new file mode 100644 index 000000000..c97f2f995 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvApmController.java @@ -0,0 +1,36 @@ +package tools.vitruv.framework.remote.common.apm; + +import java.nio.file.Path; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Metrics; + +/** + * This class allows controlling the Vitruvius monitoring. + */ +public class VitruvApmController { + private static VitruvStepMeterRegistry ACTIVE_REGISTRY; + + /** + * Enables the monitoring for Vitruvius. + * + * @param output Path to a file in which all measurements are stored. + */ + public static void enable(Path output) { + if (ACTIVE_REGISTRY == null) { + ACTIVE_REGISTRY = new VitruvStepMeterRegistry(new VitruvStepRegistryConfig(), Clock.SYSTEM, output); + Metrics.globalRegistry.add(ACTIVE_REGISTRY); + } + } + + /** + * Disables the monitoring for Vitruvius. + */ + public static void disable() { + if (ACTIVE_REGISTRY != null) { + ACTIVE_REGISTRY.stop(); + Metrics.globalRegistry.remove(ACTIVE_REGISTRY); + ACTIVE_REGISTRY = null; + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvStepMeterRegistry.java b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvStepMeterRegistry.java new file mode 100644 index 000000000..c5b46419d --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvStepMeterRegistry.java @@ -0,0 +1,63 @@ +package tools.vitruv.framework.remote.common.apm; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter.Id; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.pause.PauseDetector; +import io.micrometer.core.instrument.step.StepMeterRegistry; +import io.micrometer.core.instrument.step.StepRegistryConfig; +import io.micrometer.core.instrument.util.NamedThreadFactory; + +/** + * Provides a specialized {@link StepMeterRegistry} for Vitruvius, which stores measurements in a file. + */ +class VitruvStepMeterRegistry extends StepMeterRegistry { + private Path output; + private StepRegistryConfig config; + + VitruvStepMeterRegistry(StepRegistryConfig config, Clock clock, Path output) { + super(config, clock); + this.output = output.toAbsolutePath(); + this.config = config; + this.start(new NamedThreadFactory("vitruv-tf")); + } + + @Override + protected void publish() { + try (BufferedWriter writer = Files.newBufferedWriter(output, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { + for (var meter : getMeters()) { + if (meter instanceof SingleMeasureRecordingTimer timer) { + for (var record : timer.getRecordings()) { + writer.append(meter.getId().toString() + "," + record.unit().toMillis(record.amount()) + "\n"); + } + timer.clear(); + } else { + for (var measurement : meter.measure()) { + writer.append(meter.getId().toString() + "," + measurement.getValue() + "\n"); + } + } + } + } catch (IOException e) { + System.err.println("Could not write metrics because: " + e.getMessage()); + } + } + + @Override + protected TimeUnit getBaseTimeUnit() { + return TimeUnit.MILLISECONDS; + } + + @Override + protected Timer newTimer(Id id, DistributionStatisticConfig config, PauseDetector detector) { + return new SingleMeasureRecordingTimer(id, this.clock, config, detector, + getBaseTimeUnit(), this.config.step().toMillis(), false); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvStepRegistryConfig.java b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvStepRegistryConfig.java new file mode 100644 index 000000000..6293daa67 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/VitruvStepRegistryConfig.java @@ -0,0 +1,15 @@ +package tools.vitruv.framework.remote.common.apm; + +import io.micrometer.core.instrument.step.StepRegistryConfig; + +class VitruvStepRegistryConfig implements StepRegistryConfig { + @Override + public String get(String arg0) { + return null; + } + + @Override + public String prefix() { + return "vitruv-step-config"; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/apm/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/package-info.java new file mode 100644 index 000000000..1eb965815 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/apm/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains a first initial prototype and exploration of how Application Performance Management (APM) + * could be integrated into Vitruvius. + */ +package tools.vitruv.framework.remote.common.apm; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/ChangeType.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/ChangeType.java new file mode 100644 index 000000000..1dfaae622 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/ChangeType.java @@ -0,0 +1,31 @@ +package tools.vitruv.framework.remote.common.json; + +import tools.vitruv.change.composite.description.CompositeChange; +import tools.vitruv.change.composite.description.TransactionalChange; +import tools.vitruv.change.composite.description.VitruviusChange; + +/** + * Represents the type of the {@link VitruviusChange}. + * + * @see CompositeChange + * @see TransactionalChange + */ +public enum ChangeType { + TRANSACTIONAL, COMPOSITE, UNKNOWN; + + /** + * Returns the type of the given {@link VitruviusChange}. + * + * @param change The change to obtain the type from. + * @return The type of the change. + */ + public static ChangeType getChangeTypeOf(VitruviusChange change) { + if (change instanceof TransactionalChange) { + return TRANSACTIONAL; + } + if (change instanceof CompositeChange) { + return COMPOSITE; + } + return UNKNOWN; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/IdTransformation.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/IdTransformation.java new file mode 100644 index 000000000..c66388116 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/IdTransformation.java @@ -0,0 +1,82 @@ +package tools.vitruv.framework.remote.common.json; + +import java.nio.file.Path; +import java.util.List; + +import org.eclipse.emf.common.util.URI; + +import tools.vitruv.change.atomic.EChange; +import tools.vitruv.change.atomic.hid.HierarchicalId; +import tools.vitruv.change.atomic.root.RootEChange; +import tools.vitruv.change.utils.ProjectMarker; + +/** + * Contains functions to transform IDs used by the Vitruvius framework to identify + * {@link org.eclipse.emf.ecore.EObject EObjects}. + */ +public class IdTransformation { + private URI root; + + IdTransformation(Path vsumPath) { + root = URI.createFileURI(ProjectMarker.getProjectRootFolder(vsumPath).toString()); + + var nextToCheck = vsumPath; + while ((nextToCheck = nextToCheck.getParent()) != null) { + try { + root = URI.createFileURI(ProjectMarker.getProjectRootFolder(nextToCheck).toString()); + } catch (IllegalStateException e) { + break; + } + } + } + + /** + * Transforms the given global (absolute path) ID to a local ID (relative path). + * + * @param global The ID to transform. + * @return The local ID. + */ + public URI toLocal(URI global) { + if (global == null || global.toString().contains("cache") || + global.toString().equals(JsonFieldName.TEMP_VALUE) || !global.isFile()) { + return global; + } + + return URI.createURI(global.toString().replace(root.toString(), "")); + } + + /** + * Transforms the given local ID (relative path) to a global ID (absolute path). + * + * @param local The ID to transform. + * @return The global ID. + */ + public URI toGlobal(URI local) { + if (local == null || local.toString().contains("cache") || + local.toString().equals(JsonFieldName.TEMP_VALUE)) { + return local; + } + + if (!local.isRelative()) { + return local; + } + + return URI.createURI(root.toString() + local.toString()); + } + + public void allToGlobal(List> eChanges) { + for (var eChange : eChanges) { + if (eChange instanceof RootEChange change) { + change.setUri(toGlobal(URI.createURI(change.getUri())).toString()); + } + } + } + + public void allToLocal(List> eChanges) { + for (var eChange : eChanges) { + if (eChange instanceof RootEChange change) { + change.setUri(toLocal(URI.createURI(change.getUri())).toString()); + } + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/JsonFieldName.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/JsonFieldName.java new file mode 100644 index 000000000..1b5e4aea8 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/JsonFieldName.java @@ -0,0 +1,15 @@ +package tools.vitruv.framework.remote.common.json; + +public final class JsonFieldName { + public static final String CHANGE_TYPE = "changeType"; + public static final String E_CHANGES = "eChanges"; + public static final String V_CHANGES = "vChanges"; + public static final String U_INTERACTIONS = "uInteractions"; + public static final String TEMP_VALUE = "temp"; + public static final String CONTENT = "content"; + public static final String URI = "uri"; + + private JsonFieldName() throws InstantiationException { + throw new InstantiationException("Cannot be instantiated"); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/JsonMapper.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/JsonMapper.java new file mode 100644 index 000000000..a7c78336a --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/JsonMapper.java @@ -0,0 +1,115 @@ +package tools.vitruv.framework.remote.common.json; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emfcloud.jackson.annotations.EcoreIdentityInfo; +import org.eclipse.emfcloud.jackson.databind.EMFContext; +import org.eclipse.emfcloud.jackson.module.EMFModule; +import org.eclipse.emfcloud.jackson.module.EMFModule.Feature; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import tools.vitruv.change.composite.description.VitruviusChange; +import tools.vitruv.framework.remote.common.json.deserializer.ReferenceDeserializerModifier; +import tools.vitruv.framework.remote.common.json.deserializer.ResourceSetDeserializer; +import tools.vitruv.framework.remote.common.json.deserializer.VitruviusChangeDeserializer; +import tools.vitruv.framework.remote.common.json.serializer.ReferenceSerializerModifier; +import tools.vitruv.framework.remote.common.json.serializer.ResourceSetSerializer; +import tools.vitruv.framework.remote.common.json.serializer.VitruviusChangeSerializer; + +/** + * This mapper can be used to serialize objects and deserialize JSON in the context of Vitruvius. + * It has custom De-/Serializers for {@link ResourceSet}s, {@link Resource}s and {@link VitruviusChange}s. + */ +public class JsonMapper { + private final ObjectMapper mapper = new ObjectMapper(); + + public JsonMapper(Path vsumPath) { + final var transformation = new IdTransformation(vsumPath); + + mapper.configure(SerializationFeature.INDENT_OUTPUT, true); + var module = new EMFModule(); + + //Register serializer + module.addSerializer(ResourceSet.class, new ResourceSetSerializer(transformation)); + module.addSerializer(VitruviusChange.class, new VitruviusChangeSerializer()); + + //Register deserializer + module.addDeserializer(ResourceSet.class, new ResourceSetDeserializer(this, transformation)); + module.addDeserializer(VitruviusChange.class, new VitruviusChangeDeserializer(this, transformation)); + + //Register modifiers for references to handle HierarichalId + module.setSerializerModifier(new ReferenceSerializerModifier(transformation)); + module.setDeserializerModifier(new ReferenceDeserializerModifier(transformation)); + + //Use IDs to identify eObjects on client and server + module.configure(Feature.OPTION_USE_ID, true); + module.setIdentityInfo(new EcoreIdentityInfo("_id")); + + mapper.registerModule(module); + } + + /** + * Serializes the given object. + * + * @param obj The object to serialize. + * @return The JSON or {@code null} if an {@link JsonProcessingException} occurred. + */ + public String serialize(Object obj) throws JsonProcessingException { + return mapper.writeValueAsString(obj); + } + + /** + * Deserializes the given JSON string. + * + * @param The type of the returned object. + * @param json The JSON to deserialize. + * @param clazz The class of the JSON type. + * @return The object or {@code null} if an {@link JsonProcessingException} occurred. + */ + public T deserialize(String json, Class clazz) throws JsonProcessingException { + return mapper.reader().forType(clazz).readValue(json); + } + + /** + * Deserializes the given JSON node. + * + * @param The type of the returned object. + * @param json The JSON node to deserialize. + * @param clazz The class of the JSON type. + * @return The object. + * @throws JsonProcessingException If the JSON node cannot be processed. + * @throws IOException If there is an IO exception during deserialization. + */ + public T deserialize(JsonNode json, Class clazz) throws JsonProcessingException, IOException { + return mapper.reader().forType(clazz).readValue(json); + } + + public Resource deserializeResource(String json, String uri, ResourceSet parentSet) throws JsonProcessingException { + return mapper.reader() + .withAttribute(EMFContext.Attributes.RESOURCE_SET, parentSet) + .withAttribute(EMFContext.Attributes.RESOURCE_URI, URI.createURI(uri)) + .forType(Resource.class) + .readValue(json); + } + + /** + * Deserializes the given JSON array to a list. + * + * @param json The JSON array to deserialize. + * @param clazz The class representing the JSON type of the objects in the JSON array. + * @return The list of objects or {@code null} if an {@link JsonProcessingException} occurred. + */ + public List deserializeArrayOf(String json, Class clazz) throws JsonProcessingException { + var javaType = mapper.getTypeFactory().constructCollectionType(List.class, clazz); + return mapper.readValue(json, javaType); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/HidReferenceEntry.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/HidReferenceEntry.java new file mode 100644 index 000000000..4c6f01470 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/HidReferenceEntry.java @@ -0,0 +1,28 @@ +package tools.vitruv.framework.remote.common.json.deserializer; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emfcloud.jackson.databind.deser.ReferenceEntry; +import org.eclipse.emfcloud.jackson.handlers.URIHandler; +import org.eclipse.emfcloud.jackson.utils.EObjects; + +import com.fasterxml.jackson.databind.DatabindContext; + +import tools.vitruv.change.atomic.hid.HierarchicalId; + +public class HidReferenceEntry implements ReferenceEntry { + private final EObject owner; + private final EReference reference; + private final String hid; + + public HidReferenceEntry(EObject owner, EReference reference, String hid) { + this.owner = owner; + this.reference = reference; + this.hid = hid; + } + + @Override + public void resolve(DatabindContext context, URIHandler handler) { + EObjects.setOrAdd(owner, reference, new HierarchicalId(hid)); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/HierarichalIdDeserializer.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/HierarichalIdDeserializer.java new file mode 100644 index 000000000..3d0f394d6 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/HierarichalIdDeserializer.java @@ -0,0 +1,36 @@ +package tools.vitruv.framework.remote.common.json.deserializer; + +import java.io.IOException; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emfcloud.jackson.databind.EMFContext; +import org.eclipse.emfcloud.jackson.databind.deser.EcoreReferenceDeserializer; +import org.eclipse.emfcloud.jackson.databind.deser.ReferenceEntry; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import tools.vitruv.framework.remote.common.json.IdTransformation; + +public class HierarichalIdDeserializer extends JsonDeserializer { + private final EcoreReferenceDeserializer standardDeserializer; + private final IdTransformation transformation; + + public HierarichalIdDeserializer(EcoreReferenceDeserializer standardDeserializer, IdTransformation transformation) { + this.standardDeserializer = standardDeserializer; + this.transformation = transformation; + } + + @Override + public ReferenceEntry deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException { + if (parser.currentToken() == JsonToken.VALUE_STRING) { + var node = context.readTree(parser); + return new HidReferenceEntry(EMFContext.getParent(context), EMFContext.getReference(context), + transformation.toGlobal(URI.createURI(node.asText())).toString()); + } + return standardDeserializer.deserialize(parser, context); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/ReferenceDeserializerModifier.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/ReferenceDeserializerModifier.java new file mode 100644 index 000000000..60b81d067 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/ReferenceDeserializerModifier.java @@ -0,0 +1,28 @@ +package tools.vitruv.framework.remote.common.json.deserializer; + +import org.eclipse.emfcloud.jackson.databind.deser.EcoreReferenceDeserializer; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.type.ReferenceType; + +import tools.vitruv.framework.remote.common.json.IdTransformation; + +public class ReferenceDeserializerModifier extends BeanDeserializerModifier { + private final IdTransformation transformation; + + public ReferenceDeserializerModifier(IdTransformation transformation) { + this.transformation = transformation; + } + + @Override + public JsonDeserializer modifyReferenceDeserializer(DeserializationConfig config, ReferenceType type, + BeanDescription beanDesc, JsonDeserializer deserializer) { + if (deserializer instanceof EcoreReferenceDeserializer referenceDeserializer) { + return new HierarichalIdDeserializer(referenceDeserializer, transformation); + } + return deserializer; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/ResourceSetDeserializer.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/ResourceSetDeserializer.java new file mode 100644 index 000000000..4b095e129 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/ResourceSetDeserializer.java @@ -0,0 +1,43 @@ +package tools.vitruv.framework.remote.common.json.deserializer; + +import java.io.IOException; +import java.util.Map; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.ResourceSet; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import tools.vitruv.framework.remote.common.json.IdTransformation; +import tools.vitruv.framework.remote.common.json.JsonFieldName; +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.util.ResourceUtil; + +public class ResourceSetDeserializer extends JsonDeserializer { + private final IdTransformation transformation; + private final JsonMapper mapper; + + public ResourceSetDeserializer(JsonMapper mapper, IdTransformation transformation) { + this.transformation = transformation; + this.mapper = mapper; + } + + @Override + public ResourceSet deserialize(JsonParser parser, DeserializationContext context) throws IOException { + var rootNode = (ArrayNode) parser.getCodec().readTree(parser); + + var resourceSet = ResourceUtil.createJsonResourceSet(); + for (var e : rootNode) { + var resource = mapper.deserializeResource(e.get(JsonFieldName.CONTENT).toString(), + transformation.toGlobal(URI.createURI(e.get(JsonFieldName.URI).asText())).toString(), resourceSet); + if (!resource.getURI().toString().equals(JsonFieldName.TEMP_VALUE)) { + resource.save(Map.of()); + } + } + + return resourceSet; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/VitruviusChangeDeserializer.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/VitruviusChangeDeserializer.java new file mode 100644 index 000000000..299f7d19f --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/VitruviusChangeDeserializer.java @@ -0,0 +1,61 @@ +package tools.vitruv.framework.remote.common.json.deserializer; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import tools.vitruv.change.atomic.EChange; +import tools.vitruv.change.atomic.hid.HierarchicalId; +import tools.vitruv.change.composite.description.TransactionalChange; +import tools.vitruv.change.composite.description.VitruviusChange; +import tools.vitruv.change.composite.description.VitruviusChangeFactory; +import tools.vitruv.change.interaction.UserInteractionBase; +import tools.vitruv.framework.remote.common.json.ChangeType; +import tools.vitruv.framework.remote.common.json.IdTransformation; +import tools.vitruv.framework.remote.common.json.JsonFieldName; +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.util.ResourceUtil; + +public class VitruviusChangeDeserializer extends JsonDeserializer> { + private final IdTransformation transformation; + private final JsonMapper mapper; + + public VitruviusChangeDeserializer(JsonMapper mapper, IdTransformation transformation) { + this.mapper = mapper; + this.transformation = transformation; + } + + @Override + public VitruviusChange deserialize(JsonParser parser, DeserializationContext context) throws IOException { + var rootNode = parser.getCodec().readTree(parser); + var type = ChangeType.valueOf(((TextNode)rootNode.get(JsonFieldName.CHANGE_TYPE)).asText()); + + VitruviusChange change; + if (type == ChangeType.TRANSACTIONAL) { + var resourceNode = rootNode.get(JsonFieldName.E_CHANGES); + var changesResource = mapper.deserializeResource(resourceNode.toString(), JsonFieldName.TEMP_VALUE, ResourceUtil.createJsonResourceSet()); + @SuppressWarnings("unchecked") + List> changes = changesResource.getContents().stream().map(e -> (EChange) e).toList(); + transformation.allToGlobal(changes); + change = VitruviusChangeFactory.getInstance().createTransactionalChange(changes); + var interactions = mapper.deserializeArrayOf(rootNode.get(JsonFieldName.U_INTERACTIONS).toString(), UserInteractionBase.class); + ((TransactionalChange) change).setUserInteractions(interactions); + } else if (type == ChangeType.COMPOSITE) { + var changesNode = (ArrayNode) rootNode.get(JsonFieldName.V_CHANGES); + var changes = new LinkedList>(); + for (var e : changesNode) { + changes.add(mapper.deserialize(e, VitruviusChange.class)); + } + change = VitruviusChangeFactory.getInstance().createCompositeChange(changes); + } else { + throw new UnsupportedOperationException("Change deserialization for type" + type + " not implemented!"); + } + return change; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/package-info.java new file mode 100644 index 000000000..2d871749e --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/deserializer/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains the EMF JSON deserialization for deltas. + */ +package tools.vitruv.framework.remote.common.json.deserializer; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/package-info.java new file mode 100644 index 000000000..eb630473c --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains common utility classes for the EMF JSON de-/serialization. + */ +package tools.vitruv.framework.remote.common.json; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/HierarichalIdSerializer.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/HierarichalIdSerializer.java new file mode 100644 index 000000000..4af5b67d6 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/HierarichalIdSerializer.java @@ -0,0 +1,33 @@ +package tools.vitruv.framework.remote.common.json.serializer; + +import java.io.IOException; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emfcloud.jackson.databind.ser.EcoreReferenceSerializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import tools.vitruv.change.atomic.hid.HierarchicalId; +import tools.vitruv.framework.remote.common.json.IdTransformation; + +public class HierarichalIdSerializer extends JsonSerializer{ + private final EcoreReferenceSerializer standardSerializer; + private final IdTransformation transformation; + + public HierarichalIdSerializer(EcoreReferenceSerializer standardDeserializer, IdTransformation transformation) { + this.standardSerializer = standardDeserializer; + this.transformation = transformation; + } + + @Override + public void serialize(EObject value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value instanceof HierarchicalId hid) { + gen.writeString(transformation.toLocal(URI.createURI(hid.getId())).toString()); + } else { + standardSerializer.serialize(value, gen, serializers); + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/ReferenceSerializerModifier.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/ReferenceSerializerModifier.java new file mode 100644 index 000000000..c3fcc9446 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/ReferenceSerializerModifier.java @@ -0,0 +1,26 @@ +package tools.vitruv.framework.remote.common.json.serializer; + +import org.eclipse.emfcloud.jackson.databind.ser.EcoreReferenceSerializer; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; + +import tools.vitruv.framework.remote.common.json.IdTransformation; + +public class ReferenceSerializerModifier extends BeanSerializerModifier { + private final IdTransformation transformation; + + public ReferenceSerializerModifier(IdTransformation transformation) { + this.transformation = transformation; + } + + @Override + public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription desc, JsonSerializer serializer) { + if (serializer instanceof EcoreReferenceSerializer referenceSerializer) { + return new HierarichalIdSerializer(referenceSerializer, transformation); + } + return serializer; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/ResourceSetSerializer.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/ResourceSetSerializer.java new file mode 100644 index 000000000..ddec8a867 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/ResourceSetSerializer.java @@ -0,0 +1,33 @@ +package tools.vitruv.framework.remote.common.json.serializer; + +import java.io.IOException; + +import org.eclipse.emf.ecore.resource.ResourceSet; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import tools.vitruv.framework.remote.common.json.IdTransformation; +import tools.vitruv.framework.remote.common.json.JsonFieldName; + +public class ResourceSetSerializer extends JsonSerializer { + private final IdTransformation transformation; + + public ResourceSetSerializer(IdTransformation transformation) { + this.transformation = transformation; + } + + @Override + public void serialize(ResourceSet resourceSet, JsonGenerator generator, SerializerProvider provider) throws IOException { + generator.writeStartArray(); + var resources = resourceSet.getResources(); + for (var r : resources) { + generator.writeStartObject(); + generator.writeObjectField(JsonFieldName.URI, transformation.toLocal(r.getURI()).toString()); + generator.writeObjectField(JsonFieldName.CONTENT, r); + generator.writeEndObject(); + } + generator.writeEndArray(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/VitruviusChangeSerializer.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/VitruviusChangeSerializer.java new file mode 100644 index 000000000..1b1a25ab2 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/VitruviusChangeSerializer.java @@ -0,0 +1,41 @@ +package tools.vitruv.framework.remote.common.json.serializer; + +import java.io.IOException; + +import org.eclipse.emf.common.util.URI; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import tools.vitruv.change.composite.description.CompositeChange; +import tools.vitruv.change.composite.description.TransactionalChange; +import tools.vitruv.change.composite.description.VitruviusChange; +import tools.vitruv.framework.remote.common.json.ChangeType; +import tools.vitruv.framework.remote.common.json.JsonFieldName; +import tools.vitruv.framework.remote.common.util.ResourceUtil; + +@SuppressWarnings("rawtypes") +public class VitruviusChangeSerializer extends JsonSerializer { + @Override + public void serialize(VitruviusChange vitruviusChange, JsonGenerator generator, SerializerProvider provider) throws IOException { + generator.writeStartObject(); + generator.writeStringField(JsonFieldName.CHANGE_TYPE, ChangeType.getChangeTypeOf(vitruviusChange).toString()); + if (vitruviusChange instanceof TransactionalChange tc) { + var changesResource = ResourceUtil.createResourceWith(URI.createURI(JsonFieldName.TEMP_VALUE), tc.getEChanges()); + generator.writeFieldName(JsonFieldName.E_CHANGES); + generator.writeObject(changesResource); + generator.writeObjectField(JsonFieldName.U_INTERACTIONS,tc.getUserInteractions()); + } else if (vitruviusChange instanceof CompositeChange cc) { + var changes = cc.getChanges(); + generator.writeArrayFieldStart(JsonFieldName.V_CHANGES); + for (var change : changes) { + generator.writeObject(change); + } + generator.writeEndArray(); + } else { + throw new UnsupportedOperationException("Change serialization of type " + vitruviusChange.getClass().getName() + " not implemented!"); + } + generator.writeEndObject(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/package-info.java new file mode 100644 index 000000000..2ca314194 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/json/serializer/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains the EMF JSON serialization for deltas. + */ +package tools.vitruv.framework.remote.common.json.serializer; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/common/package-info.java new file mode 100644 index 000000000..ee08cb8ff --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/package-info.java @@ -0,0 +1,4 @@ +/** + * This package provides common utility classes for both Vitruvius server and client. + */ +package tools.vitruv.framework.remote.common; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/ContentType.java b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/ContentType.java new file mode 100644 index 000000000..df580b408 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/ContentType.java @@ -0,0 +1,10 @@ +package tools.vitruv.framework.remote.common.rest.constants; + +public final class ContentType { + public static final String APPLICATION_JSON = "application/json"; + public static final String TEXT_PLAIN = "text/plain"; + + private ContentType() throws InstantiationException { + throw new InstantiationException("Cannot be instantiated"); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/EndpointPath.java b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/EndpointPath.java new file mode 100644 index 000000000..08518ca9f --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/EndpointPath.java @@ -0,0 +1,14 @@ +package tools.vitruv.framework.remote.common.rest.constants; + +public final class EndpointPath { + public static final String HEALTH = "/health"; + public static final String VIEW_TYPES = "/vsum/view/types"; + public static final String VIEW_SELECTOR = "/vsum/view/selector"; + public static final String VIEW = "/vsum/view"; + public static final String IS_VIEW_CLOSED = "/vsum/view/closed"; + public static final String IS_VIEW_OUTDATED = "/vsum/view/outdated"; + + private EndpointPath() throws InstantiationException { + throw new InstantiationException("Cannot be instantiated"); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/Header.java b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/Header.java new file mode 100644 index 000000000..f05faeab4 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/Header.java @@ -0,0 +1,12 @@ +package tools.vitruv.framework.remote.common.rest.constants; + +public final class Header { + public static final String CONTENT_TYPE = "Content-Type"; + public static final String VIEW_UUID = "View-UUID"; + public static final String SELECTOR_UUID = "Selector-UUID"; + public static final String VIEW_TYPE = "View-Type"; + + private Header() throws InstantiationException { + throw new InstantiationException("Cannot be instantiated"); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/package-info.java new file mode 100644 index 000000000..98409a95d --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/rest/constants/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains common constants for both Vitruvius server and client. + */ +package tools.vitruv.framework.remote.common.rest.constants; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/util/ResourceUtil.java b/remote/src/main/java/tools/vitruv/framework/remote/common/util/ResourceUtil.java new file mode 100644 index 000000000..73c642f90 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/util/ResourceUtil.java @@ -0,0 +1,61 @@ +package tools.vitruv.framework.remote.common.util; + +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emfcloud.jackson.resource.JsonResourceFactory; + +/** + * Contains utility functions to work with {@link Resource}s. + */ +public class ResourceUtil { + private ResourceUtil() throws InstantiationException { + throw new InstantiationException("Cannot be instantiated"); + } + + /** + * Creates a {@link Resource} with the given {@link URI} and given content. + * + * @param uri The URI of the resource. + * @param content The content of the resource. + * @param parentSet The parent {@link ResourceSet} of the resource. + * @return The created {@link Resource}. + */ + public static Resource createResourceWith(URI uri, Collection content, ResourceSet parentSet) { + var resource = parentSet.createResource(uri); + resource.getContents().addAll(content); + return resource; + } + + /** + * Creates a {@link Resource} with the given {@link URI} and given content. + * Uses a new {@link ResourceSet} as parent set. + * + * @param uri The URI of the resource. + * @param content The content of the resource. + * @return The created {@link Resource}. + */ + public static Resource createResourceWith(URI uri, Collection content) { + return createResourceWith(uri, content, createJsonResourceSet()); + } + + public static Resource createEmptyResource(URI uri) { + return createResourceWith(uri, Collections.emptyList()); + } + + /** + * Returns a {@link ResourceSet} and registers a {@link JsonResourceFactory} as default factory. + * + * @return The created {@link ResourceSet}. + */ + public static ResourceSet createJsonResourceSet() { + var set = new ResourceSetImpl(); + set.getResourceFactoryRegistry().getExtensionToFactoryMap().put("*", new JsonResourceFactory()); + return set; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/common/util/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/common/util/package-info.java new file mode 100644 index 000000000..23ed75ab2 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/common/util/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains common utility classes for the Vitruvius server and client. + */ +package tools.vitruv.framework.remote.common.util; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/VirtualModelInitializer.java b/remote/src/main/java/tools/vitruv/framework/remote/server/VirtualModelInitializer.java new file mode 100644 index 000000000..a0bb6bf37 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/VirtualModelInitializer.java @@ -0,0 +1,17 @@ +package tools.vitruv.framework.remote.server; + +import tools.vitruv.framework.vsum.VirtualModel; + +/** + * Interface for virtual model initialization. + */ +@FunctionalInterface +public interface VirtualModelInitializer { + + /** + * Initializes the virtual model and returns it. + * + * @return the initialized model. + */ + VirtualModel init(); +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/VitruvServer.java b/remote/src/main/java/tools/vitruv/framework/remote/server/VitruvServer.java new file mode 100644 index 000000000..48bc61446 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/VitruvServer.java @@ -0,0 +1,70 @@ +package tools.vitruv.framework.remote.server; + +import java.io.IOException; + +import tools.vitruv.framework.remote.common.DefaultConnectionSettings; +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.server.http.java.VitruvJavaHttpServer; +import tools.vitruv.framework.remote.server.rest.endpoints.EndpointsProvider; +import tools.vitruv.framework.vsum.VirtualModel; + +/** + * A Vitruvius server wraps a REST-based API around a {@link VirtualModel VSUM}. Therefore, + * it takes a {@link VirtualModelInitializer} which is responsible to create an instance + * of a {@link VirtualModel virtual model}. Once the server is started, the API can be used by the + * Vitruvius client to perform remote actions on the VSUM. + */ +public class VitruvServer { + private final VitruvJavaHttpServer server; + + /** + * Creates a new {@link VitruvServer} using the given {@link VirtualModelInitializer}. + * Sets host name or IP address and port which are used to open the server. + * + * @param modelInitializer The initializer which creates an {@link VirtualModel}. + * @param port The port to open to server on. + * @param hostOrIp The host name or IP address to which the server is bound. + */ + public VitruvServer(VirtualModelInitializer modelInitializer, int port, String hostOrIp) throws IOException { + var model = modelInitializer.init(); + var mapper = new JsonMapper(model.getFolder()); + var endpoints = EndpointsProvider.getAllEndpoints(model, mapper); + + this.server = new VitruvJavaHttpServer(hostOrIp, port, endpoints); + } + + /** + * Creates a new {@link VitruvServer} using the given {@link VirtualModelInitializer}. + * Sets the port which is used to open the server on to the given one. + * + * @param modelInitializer The initializer which creates an {@link VirtualModel}. + * @param port The port to open to server on. + */ + public VitruvServer(VirtualModelInitializer modelInitializer, int port) throws IOException { + this(modelInitializer, port, DefaultConnectionSettings.STD_HOST); + } + + /** + * Creates a new {@link VitruvServer} using the given {@link VirtualModelInitializer}. + * Sets the port which is used to open the server on to 8080. + * + * @param modelInitializer The initializer which creates an {@link InternalVirtualModel}. + */ + public VitruvServer(VirtualModelInitializer modelInitializer) throws IOException { + this(modelInitializer, DefaultConnectionSettings.STD_PORT); + } + + /** + * Starts the Vitruvius server. + */ + public void start() { + server.start(); + } + + /** + * Stops the Vitruvius server. + */ + public void stop() { + server.stop(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/exception/ServerHaltingException.java b/remote/src/main/java/tools/vitruv/framework/remote/server/exception/ServerHaltingException.java new file mode 100644 index 000000000..c87435201 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/exception/ServerHaltingException.java @@ -0,0 +1,20 @@ +package tools.vitruv.framework.remote.server.exception; + +/** + * Represents an exception which should be thrown when the processing of a REST-request is halted due to an error. + * An HTTP-status code must be provided, since this exception is caught by the + * {@link tools.vitruv.framework.remote.server.VitruvServer Server} in order to create error response messages. + */ +public class ServerHaltingException extends RuntimeException { + + private final int statusCode; + + public ServerHaltingException(int statusCode, String msg) { + super(msg); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/exception/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/server/exception/package-info.java new file mode 100644 index 000000000..75a10c7f1 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/exception/package-info.java @@ -0,0 +1,4 @@ +/** + * Defines exceptions for the Vitruvius server. + */ +package tools.vitruv.framework.remote.server.exception; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/http/HttpWrapper.java b/remote/src/main/java/tools/vitruv/framework/remote/server/http/HttpWrapper.java new file mode 100644 index 000000000..3f5d42526 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/http/HttpWrapper.java @@ -0,0 +1,52 @@ +package tools.vitruv.framework.remote.server.http; + +import java.io.IOException; + +/** + * This interface wraps an HTTP request/response from the underlying HTTP server implementation. + */ +public interface HttpWrapper { + /** + * Returns a response header. + * + * @param header Name of the header. + * @return The value of the header. + */ + String getRequestHeader(String header); + /** + * Returns the request body converted to a String. + * + * @return The request body as String. + * @throws IOException If the body cannot be read or if the conversion fails. + */ + String getRequestBodyAsString() throws IOException; + + /** + * Adds a value to the response header. + * + * @param header Name of the header. + * @param value The value of the header to add. + */ + void addResponseHeader(String header, String value); + /** + * Sets the content type of the response. + * + * @param type The content type. + */ + void setContentType(String type); + /** + * Sends an HTTP response without a body. + * + * @param responseCode The status code of the response. + * @throws IOException If the response cannot be sent. + */ + void sendResponse(int responseCode) throws IOException; + /** + * Sends an HTTP response with a body. + * + * @param responseCode The status code of the response. + * @param body The body of the response. + * @throws IOException If the response cannot be sent. + */ + void sendResponse(int responseCode, byte[] body) throws IOException; +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/HttpExchangeWrapper.java b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/HttpExchangeWrapper.java new file mode 100644 index 000000000..0bc974557 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/HttpExchangeWrapper.java @@ -0,0 +1,55 @@ +package tools.vitruv.framework.remote.server.http.java; + +import com.sun.net.httpserver.HttpExchange; + +import tools.vitruv.framework.remote.common.rest.constants.Header; +import tools.vitruv.framework.remote.server.http.HttpWrapper; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * This is an implementation of the {@link HttpWrapper} for the Java built-in HTTP server. + */ +class HttpExchangeWrapper implements HttpWrapper { + private final HttpExchange exchange; + + HttpExchangeWrapper(HttpExchange exchange) { + this.exchange = exchange; + } + + @Override + public void addResponseHeader(String header, String value) { + exchange.getResponseHeaders().add(header, value); + } + + @Override + public void setContentType(String type) { + exchange.getResponseHeaders().replace(Header.CONTENT_TYPE, List.of(type)); + } + + @Override + public String getRequestHeader(String header) { + return exchange.getRequestHeaders().getFirst(header); + } + + @Override + public String getRequestBodyAsString() throws IOException { + return new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + } + + @Override + public void sendResponse(int responseCode) throws IOException { + exchange.sendResponseHeaders(responseCode, -1); + } + + @Override + public void sendResponse(int responseCode, byte[] body) throws IOException { + exchange.sendResponseHeaders(responseCode, body.length); + var outputStream = exchange.getResponseBody(); + outputStream.write(body); + outputStream.flush(); + outputStream.close(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/RequestHandler.java b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/RequestHandler.java new file mode 100644 index 000000000..263f62426 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/RequestHandler.java @@ -0,0 +1,62 @@ +package tools.vitruv.framework.remote.server.http.java; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.remote.server.exception.ServerHaltingException; +import tools.vitruv.framework.remote.server.rest.PathEndointCollector; + +import java.nio.charset.StandardCharsets; + +import static java.net.HttpURLConnection.*; + +import java.io.IOException; + +/** + * Represents an {@link HttpHandler}. + */ +class RequestHandler implements HttpHandler { + private PathEndointCollector endpoints; + + RequestHandler(PathEndointCollector endpoints) { + this.endpoints = endpoints; + } + + /** + * Handles the request when this end point is called. + * + * @param exchange An object encapsulating the HTTP request and response. + */ + @Override + public void handle(HttpExchange exchange) { + var method = exchange.getRequestMethod(); + var wrapper = new HttpExchangeWrapper(exchange); + try { + var response = switch (method) { + case "GET" -> endpoints.getEndpoint().process(wrapper); + case "PUT" -> endpoints.putEndpoint().process(wrapper); + case "POST" -> endpoints.postEndpoint().process(wrapper); + case "PATCH" -> endpoints.patchEndpoint().process(wrapper); + case "DELETE" -> endpoints.deleteEndpoint().process(wrapper); + default -> throw new ServerHaltingException(HTTP_NOT_FOUND, "Request method not supported!"); + }; + if (response != null) { + wrapper.sendResponse(HTTP_OK, response.getBytes(StandardCharsets.UTF_8)); + } else { + wrapper.sendResponse(HTTP_OK); + } + } catch (Exception exception) { + var statusCode = HTTP_INTERNAL_ERROR; + if (exception instanceof ServerHaltingException haltingException) { + statusCode = haltingException.getStatusCode(); + } + wrapper.setContentType(ContentType.TEXT_PLAIN); + try { + wrapper.sendResponse(statusCode, exception.getMessage().getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new IllegalStateException("Sending a response (" + statusCode + " " + exception.getMessage() + ") failed.", e); + } + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/VitruvJavaHttpServer.java b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/VitruvJavaHttpServer.java new file mode 100644 index 000000000..3a8e5e97b --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/VitruvJavaHttpServer.java @@ -0,0 +1,26 @@ +package tools.vitruv.framework.remote.server.http.java; + +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import tools.vitruv.framework.remote.server.rest.PathEndointCollector; + +public class VitruvJavaHttpServer { + private final HttpServer server; + + public VitruvJavaHttpServer(String host, int port, List endpoints) throws IOException { + this.server = HttpServer.create(new InetSocketAddress(host, port), 0); + endpoints.forEach(endp -> { + server.createContext(endp.path(), new RequestHandler(endp)); + }); + } + + public void start() { + server.start(); + } + + public void stop() { + server.stop(0); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/package-info.java new file mode 100644 index 000000000..99a54fbd3 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/http/java/package-info.java @@ -0,0 +1,4 @@ +/** + * This package provides the built-in Java HTTP server for Vitruvius. + */ +package tools.vitruv.framework.remote.server.http.java; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/http/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/server/http/package-info.java new file mode 100644 index 000000000..64df053c4 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/http/package-info.java @@ -0,0 +1,4 @@ +/** + * This package serves interfaces to wrap HTTP servers from the actual REST end points. + */ +package tools.vitruv.framework.remote.server.http; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/server/package-info.java new file mode 100644 index 000000000..0fd8244e1 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains the Vitruvius server implementation. + */ +package tools.vitruv.framework.remote.server; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/DeleteEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/DeleteEndpoint.java new file mode 100644 index 000000000..c8e30e600 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/DeleteEndpoint.java @@ -0,0 +1,4 @@ +package tools.vitruv.framework.remote.server.rest; + +public interface DeleteEndpoint extends RestEndpoint { +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/GetEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/GetEndpoint.java new file mode 100644 index 000000000..b3619b18d --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/GetEndpoint.java @@ -0,0 +1,4 @@ +package tools.vitruv.framework.remote.server.rest; + +public interface GetEndpoint extends RestEndpoint { +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PatchEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PatchEndpoint.java new file mode 100644 index 000000000..d4f45e86e --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PatchEndpoint.java @@ -0,0 +1,4 @@ +package tools.vitruv.framework.remote.server.rest; + +public interface PatchEndpoint extends RestEndpoint { +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PathEndointCollector.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PathEndointCollector.java new file mode 100644 index 000000000..b480e4079 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PathEndointCollector.java @@ -0,0 +1,9 @@ +package tools.vitruv.framework.remote.server.rest; + +public record PathEndointCollector( + String path, + GetEndpoint getEndpoint, + PostEndpoint postEndpoint, + PutEndpoint putEndpoint, + PatchEndpoint patchEndpoint, + DeleteEndpoint deleteEndpoint) {} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PostEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PostEndpoint.java new file mode 100644 index 000000000..58c32ebf1 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PostEndpoint.java @@ -0,0 +1,4 @@ +package tools.vitruv.framework.remote.server.rest; + +public interface PostEndpoint extends RestEndpoint { +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PutEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PutEndpoint.java new file mode 100644 index 000000000..a79c984b3 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/PutEndpoint.java @@ -0,0 +1,4 @@ +package tools.vitruv.framework.remote.server.rest; + +public interface PutEndpoint extends RestEndpoint { +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/RestEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/RestEndpoint.java new file mode 100644 index 000000000..b9ac6c448 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/RestEndpoint.java @@ -0,0 +1,37 @@ +package tools.vitruv.framework.remote.server.rest; + +import tools.vitruv.framework.remote.server.exception.ServerHaltingException; +import tools.vitruv.framework.remote.server.http.HttpWrapper; + +import static java.net.HttpURLConnection.*; + +/** + * Represents an REST endpoint. + */ +public interface RestEndpoint { + /** + * Processes a given HTTP request. + * + * @param wrapper An object wrapping an HTTP request/response. + * @throws ServerHaltingException If an internal error occurred. + */ + String process(HttpWrapper wrapper) throws ServerHaltingException; + + /** + * Halts the execution of the requested endpoint and returns the status code NOT FOUND with the given message. + * + * @param msg A message containing the reason of halting the execution. + */ + default ServerHaltingException notFound(String msg) { + return new ServerHaltingException(HTTP_NOT_FOUND, msg); + } + + /** + * Halts the execution of the requested endpoint and returns the status code INTERNAL SERVER ERROR with the given message. + * + * @param msg A message containing the reason of halting the execution. + */ + default ServerHaltingException internalServerError(String msg) { + return new ServerHaltingException(HTTP_INTERNAL_ERROR, msg); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/Cache.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/Cache.java new file mode 100644 index 000000000..58ef83ba2 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/Cache.java @@ -0,0 +1,56 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import java.util.HashMap; +import java.util.Map; + +import com.google.common.collect.BiMap; +import org.eclipse.emf.ecore.EObject; +import tools.vitruv.framework.views.View; +import tools.vitruv.framework.views.ViewSelector; + +/** + * A global cache holding {@link View}s, {@link ViewSelector}s and mappings of the form UUID <-> {@link EObject}. + */ +public class Cache { + private Cache() throws InstantiationException { + throw new InstantiationException("Cannot be instantiated"); + } + + private static final Map viewCache = new HashMap<>(); + private static final Map selectorCache = new HashMap<>(); + private static final Map> perSelectorUuidToEObjectMapping = new HashMap<>(); + + public static void addView(String uuid, View view) { + viewCache.put(uuid, view); + } + + public static View getView(String uuid) { + return viewCache.get(uuid); + } + + public static View removeView(String uuid) { + return viewCache.remove(uuid); + } + + public static void addSelectorWithMapping(String selectorUuid, ViewSelector selector, BiMap mapping) { + selectorCache.put(selectorUuid, selector); + perSelectorUuidToEObjectMapping.put(selectorUuid, mapping); + } + + public static ViewSelector getSelector(String selectorUuid) { + return selectorCache.get(selectorUuid); + } + + public static EObject getEObjectFromMapping(String selectorUuid, String objectUuid) { + return perSelectorUuidToEObjectMapping.get(selectorUuid).get(objectUuid); + } + + public static String getUuidFromMapping(String selectorUuid, EObject eObject) { + return perSelectorUuidToEObjectMapping.get(selectorUuid).inverse().get(eObject); + } + + public static void removeSelectorAndMapping(String selectorUuid) { + perSelectorUuidToEObjectMapping.remove(selectorUuid); + selectorCache.remove(selectorUuid); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ChangePropagationEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ChangePropagationEndpoint.java new file mode 100644 index 000000000..b952c323c --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ChangePropagationEndpoint.java @@ -0,0 +1,78 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import tools.vitruv.change.atomic.root.InsertRootEObject; +import tools.vitruv.change.composite.description.VitruviusChange; +import tools.vitruv.framework.remote.server.exception.ServerHaltingException; +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.PatchEndpoint; +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.rest.constants.Header; +import tools.vitruv.framework.views.impl.ModifiableView; +import tools.vitruv.framework.views.impl.ViewCreatingViewType; + +import java.io.IOException; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.impl.ResourceImpl; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; + +import static java.net.HttpURLConnection.*; + +/** + * This endpoint applies given {@link VitruviusChange}s to the VSUM. + */ +public class ChangePropagationEndpoint implements PatchEndpoint { + private static final String ENDPOINT_METRIC_NAME = "vitruv.server.rest.propagation"; + private final JsonMapper mapper; + + public ChangePropagationEndpoint(JsonMapper mapper) { + this.mapper = mapper; + } + + @SuppressWarnings("unchecked") + @Override + public String process(HttpWrapper wrapper) { + var view = Cache.getView(wrapper.getRequestHeader(Header.VIEW_UUID)); + if (view == null) { + throw notFound("View with given id not found!"); + } + + String body; + try { + body = wrapper.getRequestBodyAsString(); + } catch (IOException e) { + throw internalServerError(e.getMessage()); + } + + @SuppressWarnings("rawtypes") + VitruviusChange change; + var desTimer = Timer.start(Metrics.globalRegistry); + try { + change = mapper.deserialize(body, VitruviusChange.class); + desTimer.stop(Metrics.timer(ENDPOINT_METRIC_NAME, "deserialization", "success")); + } catch (JsonProcessingException e) { + desTimer.stop(Metrics.timer(ENDPOINT_METRIC_NAME, "deserialization", "failure")); + throw new ServerHaltingException(HTTP_BAD_REQUEST, e.getMessage()); + } + change.getEChanges().forEach(it -> { + if (it instanceof InsertRootEObject echange) { + echange.setResource(new ResourceImpl(URI.createURI(echange.getUri()))); + } + }); + + var type = (ViewCreatingViewType) view.getViewType(); + var propTimer = Timer.start(Metrics.globalRegistry); + try { + type.commitViewChanges((ModifiableView) view, change); + propTimer.stop(Metrics.timer(ENDPOINT_METRIC_NAME, "propagation", "success")); + } catch (RuntimeException e) { + propTimer.stop(Metrics.timer(ENDPOINT_METRIC_NAME, "propagation", "failure")); + throw new ServerHaltingException(HTTP_CONFLICT, "Changes rejected: " + e.getMessage()); + } + return null; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/CloseViewEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/CloseViewEndpoint.java new file mode 100644 index 000000000..670003549 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/CloseViewEndpoint.java @@ -0,0 +1,24 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.DeleteEndpoint; +import tools.vitruv.framework.remote.common.rest.constants.Header; + +/** + * This endpoint closes a {@link tools.vitruv.framework.views.View View}. + */ +public class CloseViewEndpoint implements DeleteEndpoint { + @Override + public String process(HttpWrapper wrapper) { + var view = Cache.removeView(wrapper.getRequestHeader(Header.VIEW_UUID)); + if (view == null) { + throw notFound("View with given id not found!"); + } + try { + view.close(); + return null; + } catch (Exception e) { + throw internalServerError(e.getMessage()); + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/EndpointsProvider.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/EndpointsProvider.java new file mode 100644 index 000000000..aa998f43b --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/EndpointsProvider.java @@ -0,0 +1,110 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import java.util.ArrayList; +import java.util.List; + +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.rest.constants.EndpointPath; +import tools.vitruv.framework.remote.server.exception.ServerHaltingException; +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.DeleteEndpoint; +import tools.vitruv.framework.remote.server.rest.GetEndpoint; +import tools.vitruv.framework.remote.server.rest.PatchEndpoint; +import tools.vitruv.framework.remote.server.rest.PathEndointCollector; +import tools.vitruv.framework.remote.server.rest.PostEndpoint; +import tools.vitruv.framework.remote.server.rest.PutEndpoint; +import tools.vitruv.framework.vsum.VirtualModel; + +public class EndpointsProvider { + public static List getAllEndpoints(VirtualModel virtualModel, JsonMapper mapper) { + var defaultEndpoints = getDefaultEndpoints(); + + List result = new ArrayList<>(); + result.add(new PathEndointCollector( + EndpointPath.HEALTH, + new HealthEndpoint(), + defaultEndpoints.postEndpoint(), + defaultEndpoints.putEndpoint(), + defaultEndpoints.patchEndpoint(), + defaultEndpoints.deleteEndpoint() + )); + result.add(new PathEndointCollector( + EndpointPath.IS_VIEW_CLOSED, + new IsViewClosedEndpoint(), + defaultEndpoints.postEndpoint(), + defaultEndpoints.putEndpoint(), + defaultEndpoints.patchEndpoint(), + defaultEndpoints.deleteEndpoint() + )); + result.add(new PathEndointCollector( + EndpointPath.IS_VIEW_OUTDATED, + new IsViewOutdatedEndpoint(), + defaultEndpoints.postEndpoint(), + defaultEndpoints.putEndpoint(), + defaultEndpoints.patchEndpoint(), + defaultEndpoints.deleteEndpoint() + )); + result.add(new PathEndointCollector( + EndpointPath.VIEW, + new UpdateViewEndpoint(mapper), + new ViewEndpoint(mapper), + defaultEndpoints.putEndpoint(), + new ChangePropagationEndpoint(mapper), + new CloseViewEndpoint() + )); + result.add(new PathEndointCollector( + EndpointPath.VIEW_SELECTOR, + new ViewSelectorEndpoint(virtualModel, mapper), + defaultEndpoints.postEndpoint(), + defaultEndpoints.putEndpoint(), + defaultEndpoints.patchEndpoint(), + defaultEndpoints.deleteEndpoint() + )); + result.add(new PathEndointCollector( + EndpointPath.VIEW_TYPES, + new ViewTypesEndpoint(virtualModel, mapper), + defaultEndpoints.postEndpoint(), + defaultEndpoints.putEndpoint(), + defaultEndpoints.patchEndpoint(), + defaultEndpoints.deleteEndpoint() + )); + + return result; + } + + private static PathEndointCollector getDefaultEndpoints() { + var getEndpoint = new GetEndpoint() { + @Override + public String process(HttpWrapper wrapper) throws ServerHaltingException { + throw notFound("Get mapping for this request path not found!"); + } + }; + var postEndpoint = new PostEndpoint() { + @Override + public String process(HttpWrapper wrapper) throws ServerHaltingException { + throw notFound("Post mapping for this request path not found!"); + } + }; + var patchEndpoint = new PatchEndpoint() { + @Override + public String process(HttpWrapper wrapper) throws ServerHaltingException { + throw notFound("Patch mapping for this request path not found!"); + } + }; + var deleteEndpoint = new DeleteEndpoint() { + @Override + public String process(HttpWrapper wrapper) throws ServerHaltingException { + throw notFound("Delete mapping for this request path not found!"); + } + }; + var putEndpoint = new PutEndpoint() { + @Override + public String process(HttpWrapper wrapper) throws ServerHaltingException { + throw notFound("Put mapping for this request path not found!"); + } + }; + return new PathEndointCollector("", getEndpoint, postEndpoint, putEndpoint, patchEndpoint, deleteEndpoint); + } + + private EndpointsProvider() {} +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/HealthEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/HealthEndpoint.java new file mode 100644 index 000000000..f55868c63 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/HealthEndpoint.java @@ -0,0 +1,16 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.GetEndpoint; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; + +/** + * This endpoint can be used to check, if the server is running. + */ +public class HealthEndpoint implements GetEndpoint { + @Override + public String process(HttpWrapper wrapper) { + wrapper.setContentType(ContentType.TEXT_PLAIN); + return "Vitruv server up and running!"; + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/IsViewClosedEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/IsViewClosedEndpoint.java new file mode 100644 index 000000000..01b2ee150 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/IsViewClosedEndpoint.java @@ -0,0 +1,24 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.GetEndpoint; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.remote.common.rest.constants.Header; + +/** + * This endpoint returns whether a {@link tools.vitruv.framework.views.View View} is closed. + */ +public class IsViewClosedEndpoint implements GetEndpoint { + @Override + public String process(HttpWrapper wrapper) { + var view = Cache.getView(wrapper.getRequestHeader(Header.VIEW_UUID)); + if (view == null) { + return Boolean.TRUE.toString(); + } + if (view.isClosed()) { + Cache.removeView(wrapper.getRequestHeader(Header.VIEW_UUID)); + } + wrapper.setContentType(ContentType.TEXT_PLAIN); + return view.isClosed() ? Boolean.TRUE.toString() : Boolean.FALSE.toString(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/IsViewOutdatedEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/IsViewOutdatedEndpoint.java new file mode 100644 index 000000000..9fbbc7ad5 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/IsViewOutdatedEndpoint.java @@ -0,0 +1,21 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.GetEndpoint; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.remote.common.rest.constants.Header; + +/** + * This view returns whether a {@link tools.vitruv.framework.views.View View} is outdated. + */ +public class IsViewOutdatedEndpoint implements GetEndpoint { + @Override + public String process(HttpWrapper wrapper) { + var view = Cache.getView(wrapper.getRequestHeader(Header.VIEW_UUID)); + if (view == null) { + throw notFound("View with given id not found!"); + } + wrapper.setContentType(ContentType.TEXT_PLAIN); + return view.isOutdated() ? Boolean.TRUE.toString() : Boolean.FALSE.toString(); + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/UpdateViewEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/UpdateViewEndpoint.java new file mode 100644 index 000000000..d861194c1 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/UpdateViewEndpoint.java @@ -0,0 +1,49 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import edu.kit.ipd.sdq.commons.util.org.eclipse.emf.ecore.resource.ResourceCopier; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; + +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.remote.common.rest.constants.Header; +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.GetEndpoint; + +/** + * This endpoint updates a {@link tools.vitruv.framework.views.View View} and returns the + * updated {@link org.eclipse.emf.ecore.resource.Resource Resources}. + */ +public class UpdateViewEndpoint implements GetEndpoint { + private final JsonMapper mapper; + + public UpdateViewEndpoint(JsonMapper mapper) { + this.mapper = mapper; + } + + @Override + public String process(HttpWrapper wrapper) { + var view = Cache.getView(wrapper.getRequestHeader(Header.VIEW_UUID)); + if (view == null) { + throw notFound("View with given id not found!"); + } + + view.update(); + + // Get resources. + var resources = view.getRootObjects().stream().map(EObject::eResource).distinct().toList(); + var set = new ResourceSetImpl(); + ResourceCopier.copyViewResources(resources, set); + + wrapper.setContentType(ContentType.APPLICATION_JSON); + + try { + return mapper.serialize(set); + } catch (JsonProcessingException e) { + throw internalServerError(e.getMessage()); + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewEndpoint.java new file mode 100644 index 000000000..507ec0b24 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewEndpoint.java @@ -0,0 +1,68 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import java.io.IOException; +import java.util.UUID; + +import edu.kit.ipd.sdq.commons.util.org.eclipse.emf.ecore.resource.ResourceCopier; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; + +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.remote.common.rest.constants.Header; +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.PostEndpoint; + +/** + * This endpoint returns a serialized {@link tools.vitruv.framework.views.View View} for the given + * {@link tools.vitruv.framework.views.ViewType ViewType}. + */ +public class ViewEndpoint implements PostEndpoint { + private final JsonMapper mapper; + + public ViewEndpoint(JsonMapper mapper) { + this.mapper = mapper; + } + + @Override + public String process(HttpWrapper wrapper) { + var selectorUuid = wrapper.getRequestHeader(Header.SELECTOR_UUID); + var selector = Cache.getSelector(selectorUuid); + + // Check if view type exists. + if (selector == null) { + throw notFound("Selector with UUID " + selectorUuid + " not found!"); + } + + try { + var body = wrapper.getRequestBodyAsString(); + var selection = mapper.deserializeArrayOf(body, String.class); + + // Select elements using IDs sent from client. + selection.forEach(it -> { + var object = Cache.getEObjectFromMapping(selectorUuid, it); + if (object != null) { + selector.setSelected(object, true); + } + }); + + // Create and cache view. + var uuid = UUID.randomUUID().toString(); + var view = selector.createView(); + Cache.addView(uuid, view); + Cache.removeSelectorAndMapping(selectorUuid); + + // Get resources. + var resources = view.getRootObjects().stream().map(EObject::eResource).distinct().toList(); + var set = new ResourceSetImpl(); + ResourceCopier.copyViewResources(resources, set); + + wrapper.setContentType(ContentType.APPLICATION_JSON); + wrapper.addResponseHeader(Header.VIEW_UUID, uuid); + + return mapper.serialize(set); + } catch (IOException e) { + throw internalServerError(e.getMessage()); + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewSelectorEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewSelectorEndpoint.java new file mode 100644 index 000000000..90bd4e296 --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewSelectorEndpoint.java @@ -0,0 +1,70 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.HashBiMap; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.emfcloud.jackson.resource.JsonResource; + +import tools.vitruv.framework.remote.common.json.JsonFieldName; +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.remote.common.rest.constants.Header; +import tools.vitruv.framework.remote.common.util.*; +import tools.vitruv.framework.remote.server.exception.ServerHaltingException; +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.GetEndpoint; +import tools.vitruv.framework.vsum.VirtualModel; + +import java.util.UUID; + +public class ViewSelectorEndpoint implements GetEndpoint { + private final VirtualModel model; + private final JsonMapper mapper; + + public ViewSelectorEndpoint(VirtualModel model, JsonMapper mapper) { + this.model = model; + this.mapper = mapper; + } + + @Override + public String process(HttpWrapper wrapper) throws ServerHaltingException { + var viewTypeName = wrapper.getRequestHeader(Header.VIEW_TYPE); + var types = model.getViewTypes(); + var viewType = types.stream().filter(it -> it.getName().equals(viewTypeName)).findFirst().orElse(null); + + // Check if view type exists. + if (viewType == null) { + throw notFound("View Type with name " + viewTypeName + " not found!"); + } + + // Generate selector UUID. + var selectorUuid = UUID.randomUUID().toString(); + + var selector = model.createSelector(viewType); + var originalSelection = selector.getSelectableElements().stream().toList(); + var copiedSelection = EcoreUtil.copyAll(originalSelection).stream().toList(); + + // Wrap selection in resource for serialization. + var resource = (JsonResource) ResourceUtil.createResourceWith(URI.createURI(JsonFieldName.TEMP_VALUE), copiedSelection); + + // Create EObject to UUID mapping. + HashBiMap mapping = HashBiMap.create(); + for (int i = 0; i < originalSelection.size(); i++) { + var objectUuid = UUID.randomUUID().toString(); + mapping.put(objectUuid, originalSelection.get(i)); + resource.setID(copiedSelection.get(i), objectUuid); + } + Cache.addSelectorWithMapping(selectorUuid, selector, mapping); + + wrapper.setContentType(ContentType.APPLICATION_JSON); + wrapper.addResponseHeader(Header.SELECTOR_UUID, selectorUuid); + + try { + return mapper.serialize(resource); + } catch (JsonProcessingException e) { + throw internalServerError(e.getMessage()); + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewTypesEndpoint.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewTypesEndpoint.java new file mode 100644 index 000000000..36671dfbb --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/ViewTypesEndpoint.java @@ -0,0 +1,37 @@ +package tools.vitruv.framework.remote.server.rest.endpoints; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Collection; +import java.util.List; +import tools.vitruv.framework.remote.server.http.HttpWrapper; +import tools.vitruv.framework.remote.server.rest.GetEndpoint; +import tools.vitruv.framework.remote.common.json.JsonMapper; +import tools.vitruv.framework.remote.common.rest.constants.ContentType; +import tools.vitruv.framework.views.ViewType; +import tools.vitruv.framework.vsum.VirtualModel; + +/** + * This end point returns a list of names of all registered {@link ViewType}s in the VSUM. + */ +public class ViewTypesEndpoint implements GetEndpoint { + private final VirtualModel model; + private final JsonMapper mapper; + + public ViewTypesEndpoint(VirtualModel model, JsonMapper mapper) { + this.model = model; + this.mapper = mapper; + } + + @Override + public String process(HttpWrapper wrapper) { + Collection> types = model.getViewTypes(); + List names = types.stream().map(ViewType::getName).toList(); + + wrapper.setContentType(ContentType.APPLICATION_JSON); + try { + return mapper.serialize(names); + } catch (JsonProcessingException e) { + throw internalServerError(e.getMessage()); + } + } +} diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/package-info.java new file mode 100644 index 000000000..07633342e --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/endpoints/package-info.java @@ -0,0 +1,4 @@ +/** + * This package implements the REST API end points for the Vitruvius server. + */ +package tools.vitruv.framework.remote.server.rest.endpoints; diff --git a/remote/src/main/java/tools/vitruv/framework/remote/server/rest/package-info.java b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/package-info.java new file mode 100644 index 000000000..1277b730c --- /dev/null +++ b/remote/src/main/java/tools/vitruv/framework/remote/server/rest/package-info.java @@ -0,0 +1,4 @@ +/** + * This package defines interfaces for REST API end points. They are independent of the underlying HTTP server. + */ +package tools.vitruv.framework.remote.server.rest;