Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ platform API version.
it and what it feeds, leaving its members as they are. Drawn as GLSP
feedback, so it survives a relayout or an edit.

### Changed

- **The Java ELK server is rebuilt on ELK 0.10.0, and elkjs is pinned to
0.10.0**, so a graph lays out the same on either engine; before, the
server's ELK 0.8.1 put some graphs' nodes in another order than elkjs 0.9.3.
The server's source, recovered from the shipped jar, now lives in
`elk-server/` (Maven, `mvn package`) under the EPL-2.0 only, as before, its
licence now inside the jar too; wire format unchanged (the gzip-framed
socket mode included). `test/elk-server-parity.test.ts` lays two real
documents out on both engines and requires them to match. **Products must
rename the jar they copy and name in their profile to
`elk-server-0.3.0-all.jar`**, and the server now needs **Java 17** (ELK 0.10
is built for it); without it layouts stay on elkjs, as before.

### Tooling

- `scripts/render-svg --select <id>` selects an element (the diagram's id or
Expand Down
1 change: 1 addition & 0 deletions elk-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
292 changes: 292 additions & 0 deletions elk-server/LICENSE

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions elk-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# elk-server

The Java ELK layout server the diagram server sends large graphs to
(`packages/diagram-server/src/server/elk-server/`). It is TypeFox's elk-server
(`io.typefox.elkserver`) with a gzip-framed socket mode added for
ascpp-graph-sprotty (its commit `af9cc65`).

## Where the source comes from

Only the built jar was ever committed anywhere (`elk-server-0.2.0-all.jar`, in
ascpp-graph-sprotty, mlir-viewer and here). These six classes were recovered
from it with the CFR decompiler (0.152), and are otherwise as the jar had them:
the same logic, wire format and log lines; CFR's expanded `try` blocks were
written back as try-with-resources.

The recovery was checked by building it on the ELK the jar bundled
(`-Delk.version=0.8.1 -Dmaven.compiler.release=11`) and laying out the graphs
the platform builds for six documents (a chain whose every id holds braces, two
grouped specification documents, a 148-node one, two HDL modules) on both jars
through the platform's gzip-framed client: every node, port and route came out
identical.

## The protocol

`java -jar elk-server-<version>-all.jar --socket` listens on
`ELK_SERVER_HOST`:`ELK_SERVER_PORT` (or the `elk.server.host` and
`elk.server.port` system properties; default localhost:5008) and logs
`ELK Gzip-Framed Server listening on <host>/<address>:<port>` when ready.

Each connection is framed or not by its first bytes:

- **Framed** (the only one the platform uses): every message, both ways, is the
magic `GZ10`, the body's length in 4 bytes big-endian, and the body: gzip
JSON. A request is an ELK JSON graph; the answer maps each element id to
`{ position, size }`, or an edge id to `{ route }`, or is an error
`{ message, name, stack }`. A body that is not gzip fails the connection
without an answer, so a client compresses every request, however small.
- **Legacy**: plain JSON graphs back to back, found by counting braces, those
in strings included. A brace inside an id leaves it waiting forever.

`--stdio` serves the legacy form on stdin and stdout.

## Building

```bash
mvn -q package # target/elk-server-0.3.0-all.jar
cp target/elk-server-0.3.0-all.jar ../packages/diagram-server/resources/elk-server/
```

Maven 3.9 and a JDK 17. The shade plugin merges every dependency into the one
jar, keeping each ELK bundle's list of layout providers.

## ELK and elkjs

The server is built on the ELK the platform's elkjs is: ELK 0.10.0 and elkjs
0.10.0 lay every graph the parity test holds them to out alike, node for node
and route for route. elkjs 0.9.x matched no ELK release (0.9.1 and 0.10.0 each
differed on some graphs), which is why elkjs is pinned to an exact version.
To move to another ELK, raise `elk.version` here and `elkjs` in
`packages/diagram-server/package.json` to the same release, rebuild, copy the
jar, and run `test/elk-server-parity.test.ts`.

ELK 0.10 is built for Java 17, so the server needs Java 17 or later.

## Licence

The Eclipse Public License 2.0 only, like ELK and the server this was recovered
from: this directory and the jar are not under the GPL secondary licence the
rest of the repository is also offered under. The text is in [`LICENSE`](LICENSE),
each source file says so (`SPDX-License-Identifier: EPL-2.0`), and the build
puts the same file in the jar at `META-INF/elk-server/LICENSE`, beside the
bundled libraries' own notices (`META-INF/LICENSE` there is Guava's Apache
License).
137 changes: 137 additions & 0 deletions elk-server/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The Java ELK layout server, as one fat jar:
`mvn -q package` writes target/elk-server-<version>-all.jar.

`-Delk.version=0.8.1 -Dmaven.compiler.release=11` builds it on the ELK the
original 0.2.0 jar bundled, to check that a rebuild lays a graph out exactly
as that jar did.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>io.typefox</groupId>
<artifactId>elk-server</artifactId>
<version>0.3.0</version>
<packaging>jar</packaging>

<!-- EPL-2.0 only, like ELK: not the rest of the repository's GPL secondary licence. -->
<licenses>
<license>
<name>EPL-2.0</name>
<url>https://www.eclipse.org/legal/epl-2.0/</url>
</license>
</licenses>

<properties>
<!-- The same ELK as the platform's elkjs: ELK 0.10.0 and elkjs 0.10.0 lay every
graph out alike (the elk-server parity test holds them to it), where elkjs
0.9.x matches no ELK release. Raise the two together. -->
<elk.version>0.10.0</elk.version>
<gson.version>2.10</gson.version>
<!-- ELK 0.10 is built for Java 17. -->
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.eclipse.elk</groupId>
<artifactId>org.eclipse.elk.core</artifactId>
<version>${elk.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.elk</groupId>
<artifactId>org.eclipse.elk.graph</artifactId>
<version>${elk.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.elk</groupId>
<artifactId>org.eclipse.elk.graph.json</artifactId>
<version>${elk.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.elk</groupId>
<artifactId>org.eclipse.elk.alg.layered</artifactId>
<version>${elk.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.elk</groupId>
<artifactId>org.eclipse.elk.alg.force</artifactId>
<version>${elk.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.elk</groupId>
<artifactId>org.eclipse.elk.alg.mrtree</artifactId>
<version>${elk.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
</dependencies>

<build>
<resources>
<!-- The server's licence, inside the jar beside the bundled libraries' own:
META-INF/LICENSE there is Guava's Apache License. -->
<resource>
<directory>${project.basedir}</directory>
<includes>
<include>LICENSE</include>
</includes>
<targetPath>META-INF/elk-server</targetPath>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>elk-server-${project.version}-all</finalName>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<!-- Signatures of the dependencies no longer match once merged. -->
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<!-- Each ELK bundle lists its layout providers here: keep every list. -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>io.typefox.elkserver.ElkServerApplication</mainClass>
<manifestEntries>
<Implementation-Version>${project.version}</Implementation-Version>
<ELK-Version>${elk.version}</ELK-Version>
<Bundle-License>EPL-2.0</Bundle-License>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
124 changes: 124 additions & 0 deletions elk-server/src/main/java/io/typefox/elkserver/AbstractElkServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: EPL-2.0
package io.typefox.elkserver;

import com.google.gson.Gson;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.elk.alg.force.options.ForceMetaDataProvider;
import org.eclipse.elk.alg.layered.options.LayeredMetaDataProvider;
import org.eclipse.elk.alg.mrtree.options.MrTreeMetaDataProvider;
import org.eclipse.elk.core.data.LayoutMetaDataService;
import org.eclipse.elk.graph.ElkBendPoint;
import org.eclipse.elk.graph.ElkEdge;
import org.eclipse.elk.graph.ElkEdgeSection;
import org.eclipse.elk.graph.ElkLabel;
import org.eclipse.elk.graph.ElkNode;
import org.eclipse.elk.graph.ElkPort;
import org.eclipse.elk.graph.ElkShape;
import org.eclipse.elk.graph.json.ElkGraphJson;

/** A layout server: reads ELK JSON graphs, lays them out, answers with the positions, sizes and routes. */
public abstract class AbstractElkServer {

private static final int STRING_BUILDER_CAPACITY = 262144;

public void start() {
initialize();
try {
run();
} catch (Throwable thr) {
thr.printStackTrace();
System.exit(1);
}
}

protected void initialize() {
LayoutMetaDataService.getInstance().registerLayoutMetaDataProviders(
new LayeredMetaDataProvider(), new ForceMetaDataProvider(), new MrTreeMetaDataProvider());
}

protected abstract void run() throws Exception;

/**
* One graph from a stream of concatenated JSON objects, found by counting
* braces, those inside strings included: a brace in an id leaves it waiting.
* The framed socket mode does not use it.
*/
protected ElkNode readGraph(InputStream input) throws IOException {
int objLevel = 0;
StringBuilder strBuilder = new StringBuilder(STRING_BUILDER_CAPACITY);
do {
int ch = input.read();
if (ch < 0) {
return null;
}
if (ch == '{') {
objLevel++;
} else if (ch == '}') {
objLevel--;
}
strBuilder.append((char) ch);
} while (objLevel > 0);
return ElkGraphJson.forGraph(strBuilder.toString()).toElk();
}

protected void writeLayoutData(ElkNode graph, Appendable output, Gson gson) {
Map<String, JsonObjects.LayoutElement> data = new LinkedHashMap<>();
computeLayoutData(graph, data);
gson.toJson(data, output);
}

protected void computeLayoutData(ElkNode node, Map<String, JsonObjects.LayoutElement> data) {
if (node.getIdentifier() != null) {
data.put(node.getIdentifier(), getShapeLayout(node));
}
for (ElkLabel label : node.getLabels()) {
if (label.getIdentifier() != null) {
data.put(label.getIdentifier(), getShapeLayout(label));
}
}
for (ElkPort port : node.getPorts()) {
if (port.getIdentifier() != null) {
data.put(port.getIdentifier(), getShapeLayout(port));
}
for (ElkLabel label : port.getLabels()) {
if (label.getIdentifier() != null) {
data.put(label.getIdentifier(), getShapeLayout(label));
}
}
}
for (ElkEdge edge : node.getContainedEdges()) {
if (edge.getIdentifier() != null) {
data.put(edge.getIdentifier(), getEdgeLayout(edge));
}
}
for (ElkNode child : node.getChildren()) {
computeLayoutData(child, data);
}
}

protected JsonObjects.ShapeLayoutElement getShapeLayout(ElkShape shape) {
JsonObjects.Point position = new JsonObjects.Point(shape.getX(), shape.getY());
JsonObjects.Dimension size = new JsonObjects.Dimension(shape.getWidth(), shape.getHeight());
return new JsonObjects.ShapeLayoutElement(position, size);
}

protected JsonObjects.EdgeLayoutElement getEdgeLayout(ElkEdge edge) {
if (edge.getSections().isEmpty()) {
return new JsonObjects.EdgeLayoutElement(Collections.emptyList());
}
ElkEdgeSection section = edge.getSections().get(0);
List<JsonObjects.Point> routingPoints = new ArrayList<>();
routingPoints.add(new JsonObjects.Point(section.getStartX(), section.getStartY()));
for (ElkBendPoint bendPoint : section.getBendPoints()) {
routingPoints.add(new JsonObjects.Point(bendPoint.getX(), bendPoint.getY()));
}
routingPoints.add(new JsonObjects.Point(section.getEndX(), section.getEndY()));
return new JsonObjects.EdgeLayoutElement(routingPoints);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: EPL-2.0
/*
* The ELK layout server: TypeFox's elk-server 0.2.0, with the gzip-framed socket
* mode added for ascpp-graph-sprotty (af9cc65). Recovered from the shipped
* elk-server-0.2.0-all.jar (decompiled with CFR 0.152), its logic, wire format and
* log lines unchanged; see ../../../../../../README.md.
*/
package io.typefox.elkserver;

import java.util.Arrays;
import java.util.List;

public class ElkServerApplication {

public static void main(String[] args) {
List<String> argsList = Arrays.asList(args);
boolean stdio = argsList.contains("--stdio");
boolean socket = argsList.contains("--socket");
if (!stdio && !socket) {
System.err.println("The server must be started with either --stdio or --socket argument.");
System.exit(1);
}
if (stdio && socket) {
System.err.println("The --stdio and --socket arguments cannot be used at the same time.");
System.exit(1);
}
if (stdio) {
new StdioElkServer().start();
}
if (socket) {
new GzipFramedSocketElkServer().start();
}
}
}
Loading
Loading