org.marid.runtime.internal.WineryRuntime Maven / Gradle / Ivy
package org.marid.runtime.internal;
/*-
* #%L
* marid-runtime
* %%
* Copyright (C) 2012 - 2019 MARID software development group
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
* #L%
*/
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import org.marid.runtime.model.ModelObjectFactory;
import org.marid.runtime.model.WineryImpl;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.IOException;
import java.io.StreamCorruptedException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipInputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
public final class WineryRuntime implements AutoCloseable {
private final Thread thread;
private final LinkedTransferQueue queue = new LinkedTransferQueue<>();
private final LinkedHashMap cellars;
private final AutoCloseable destroyAction;
final URLClassLoader classLoader;
final WineryImpl winery;
final ConcurrentLinkedDeque> racks;
private volatile State state = State.NEW;
private volatile Throwable startError;
private volatile Throwable destroyError;
private WineryRuntime(WineryParams params) {
this.cellars = new LinkedHashMap<>(params.winery.getCellars().size());
this.classLoader = params.classLoader;
this.destroyAction = params.destroyAction;
this.winery = params.winery;
this.racks = new ConcurrentLinkedDeque<>();
this.thread = new Thread(null, () -> {
try {
while (!Thread.currentThread().isInterrupted()) {
final var task = queue.poll(10L, TimeUnit.MILLISECONDS);
if (task == null) {
continue;
}
switch (task) {
case START:
try {
run();
} catch (Throwable e) {
startError = e;
}
break;
case STOP:
try {
destroy();
} catch (Throwable e) {
destroyError = e;
}
break;
}
}
} catch (InterruptedException e) {
// exit thread
} finally {
destroy();
}
}, winery.getName(), 96L << 10);
}
public WineryRuntime(URL zipFile, List args) {
this(new WineryParams(zipFile, args));
}
@TestOnly public WineryRuntime(ClassLoader classLoader, WineryImpl winery, AutoCloseable destroyAction) {
this(new WineryParams(new URLClassLoader(new URL[0], classLoader), winery, destroyAction));
}
@TestOnly public WineryRuntime(WineryImpl winery, AutoCloseable destroyAction) {
this(Thread.currentThread().getContextClassLoader(), winery, destroyAction);
}
@TestOnly public WineryRuntime(WineryImpl winery) {
this(winery, () -> {});
}
private static void unpack(Path deployment, ZipInputStream zipInputStream) throws IOException {
for (var e = zipInputStream.getNextEntry(); e != null; e = zipInputStream.getNextEntry()) {
try {
final var target = deployment.resolve(e.getName());
if (!target.startsWith(deployment)) {
throw new StreamCorruptedException("Invalid entry: " + e.getName());
}
if (target.equals(deployment)) {
continue;
}
if (e.isDirectory()) {
Files.createDirectory(target);
} else {
Files.copy(zipInputStream, target);
}
Files.setLastModifiedTime(target, e.getLastModifiedTime());
} finally {
zipInputStream.closeEntry();
}
}
}
private static void validate(Path resources, Path deps, Path classes) throws IOException {
Files.createDirectories(resources);
Files.createDirectories(deps);
Files.createDirectories(classes);
}
private static void initialize(Path deployment, List args) throws IOException {
final var propsFile = deployment.resolve("system.properties");
if (Files.isRegularFile(propsFile)) {
final var props = new Properties();
try (final var reader = Files.newBufferedReader(propsFile, UTF_8)) {
props.load(reader);
}
props.forEach(System.getProperties()::putIfAbsent);
}
for (final var arg : args) {
if (arg.startsWith("--") && arg.contains("=")) {
final var eqi = arg.indexOf('=');
final var key = arg.substring(2, eqi);
final var val = arg.substring(eqi + 1).trim();
System.getProperties().putIfAbsent(key, val);
}
}
}
public @NotNull String getId() {
return winery.getName();
}
public @NotNull CellarRuntime getCellar(@NotNull String name) {
return Objects.requireNonNull(cellars.get(name), () -> "No such cellar in " + winery.getName() + ": " + name);
}
public @NotNull Set<@NotNull String> getCellarNames() {
return Collections.unmodifiableSet(cellars.keySet());
}
public void start() {
switch (thread.getState()) {
case NEW:
thread.start();
break;
case TERMINATED:
throw new IllegalStateException();
}
switch (state) {
case STARTING:
case RUNNING:
case TERMINATING:
return;
case NEW:
case TERMINATED:
queue.add(Command.START);
while (state != State.RUNNING && state != State.TERMINATED) {
Thread.onSpinWait();
}
if (startError != null) {
try {
throw startError;
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new IllegalStateException(e);
} finally {
startError = null;
}
}
break;
}
}
private void run() {
if (state == State.STARTING || state == State.RUNNING) {
return;
}
state = State.STARTING;
Thread.currentThread().setContextClassLoader(classLoader);
winery.getCellars().forEach(c -> cellars.put(c.getName(), new CellarRuntime(this, c)));
try {
cellars.forEach((name, c) -> c.cellar.getConstants().forEach(e -> c.getOrCreateConst(e, new LinkedHashSet<>())));
cellars.forEach((name, c) -> c.cellar.getRacks().forEach(e -> c.getOrCreateRack(e, new LinkedHashSet<>())));
state = State.RUNNING;
} catch (Throwable e) {
try {
destroy();
} catch (Throwable x) {
e.addSuppressed(x);
}
throw e;
}
}
private void destroy() {
if (state != State.RUNNING && state != State.STARTING) {
return;
}
state = State.TERMINATING;
final var exception = new IllegalStateException("Unable to close winery " + getId());
for (final var i = racks.descendingIterator(); i.hasNext(); ) {
final var rackEntry = i.next();
try {
final var cellar = getCellar(rackEntry.getKey());
try (final var rack = cellar.getRack(rackEntry.getValue())) {
cellar.racks.remove(rack.getName());
}
} catch (Throwable e) {
exception.addSuppressed(e);
} finally {
i.remove();
}
}
cellars.forEach((name, c) -> {
try {
c.close();
} catch (Throwable e) {
exception.addSuppressed(e);
}
});
cellars.clear();
if (classLoader != null) {
try {
classLoader.close();
} catch (Throwable e) {
exception.addSuppressed(e);
}
}
try {
destroyAction.close();
} catch (Throwable e) {
exception.addSuppressed(e);
}
state = State.TERMINATED;
if (exception.getSuppressed().length > 0) {
throw exception;
}
}
@Override
public void close() throws Exception {
queue.add(Command.STOP);
while (state != State.TERMINATED) {
Thread.onSpinWait();
}
if (destroyError != null) {
try {
throw destroyError;
} catch (Exception | Error e) {
throw e;
} catch (Throwable e) {
throw new IllegalStateException(e);
} finally {
destroyError = null;
}
}
}
@Override
public @NotNull String toString() {
return getId();
}
public enum State {NEW, STARTING, RUNNING, TERMINATING, TERMINATED}
public enum Command {START, STOP}
private static final class WineryParams {
private final URLClassLoader classLoader;
private final WineryImpl winery;
private final AutoCloseable destroyAction;
private WineryParams(URL zipFile, List args) {
final Path deployment;
try {
deployment = Files.createTempDirectory("marid");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
destroyAction = () -> {
final var p = this;
if (p.classLoader != null) {
try {
p.classLoader.close();
} finally {
deleteRecursively(deployment);
}
} else {
deleteRecursively(deployment);
}
};
try {
try (final var is = new ZipInputStream(zipFile.openStream(), UTF_8)) {
unpack(deployment, is);
}
final var classes = deployment.resolve("classes");
final var resources = deployment.resolve("resources");
final var deps = deployment.resolve("deps");
final var winery = deployment.resolve("winery.xml");
final var documentBuilder = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder();
final var document = documentBuilder.parse(winery.toFile());
this.winery = (WineryImpl) ModelObjectFactory.FACTORY.newWinery();
this.winery.readFrom(document.getDocumentElement());
validate(resources, deps, classes);
initialize(deployment, args);
classLoader = classLoader(classes, resources, deps);
} catch (Throwable e) {
try {
destroyAction.close();
} catch (Throwable x) {
e.addSuppressed(x);
}
throw new IllegalStateException(e);
}
}
private WineryParams(URLClassLoader classLoader, WineryImpl winery, AutoCloseable destroyAction) {
this.classLoader = classLoader;
this.winery = winery;
this.destroyAction = destroyAction;
}
private URLClassLoader classLoader(Path classes, Path resources, Path deps) throws IOException {
final var urls = new ArrayList();
urls.add(classes.toUri().toURL());
urls.add(resources.toUri().toURL());
try (final var dirStream = Files.newDirectoryStream(deps, "*.jar")) {
for (final var path : dirStream) {
urls.add(path.toUri().toURL());
}
}
return new URLClassLoader(urls.toArray(URL[]::new), Thread.currentThread().getContextClassLoader());
}
private void deleteRecursively(Path path) throws IOException {
Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.deleteIfExists(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.deleteIfExists(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy