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 extends ViewSelector> 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 extends EChange> eChanges) {
+ for (var eChange : eChanges) {
+ if (eChange instanceof RootEChange> change) {
+ change.setUri(toGlobal(URI.createURI(change.getUri())).toString());
+ }
+ }
+ }
+
+ public void allToLocal(List extends EChange> 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 extends EObject> 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 extends EObject> 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;