com.metreeca.mark.tasks.Serve Maven / Gradle / Ivy
/*
* Copyright © 2019-2023 Metreeca srl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.metreeca.mark.tasks;
import com.metreeca.core.services.Logger;
import com.metreeca.core.toolkits.Resources;
import com.metreeca.http.Request;
import com.metreeca.http.Response;
import com.metreeca.http.codecs.Data;
import com.metreeca.http.handlers.*;
import com.metreeca.jse.JSEServer;
import com.metreeca.json.codecs.JSON;
import com.metreeca.mark.*;
import org.apache.maven.plugin.logging.Log;
import java.awt.Desktop;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.*;
import javax.json.Json;
import static com.metreeca.core.services.Logger.logger;
import static com.metreeca.http.Handler.handler;
import static com.metreeca.http.Response.OK;
import static java.lang.String.format;
/**
* Site serving task.
*
* Generates a processed version of the {@linkplain Opts#source() source} site folder in the
* {@linkplain Opts#target() target} site folder and serves it on a development grade server for testing purposes,
* watching the {@linkplain Opts#source() source} site folder for further changes to by synchronized.
*/
public final class Serve implements Task {
private static final String ReloadQueue="/~";
private static final String ReloadScript="/~script";
private static final String ReloadTag=format("", ReloadScript);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private final InetSocketAddress address=new InetSocketAddress("localhost", 2020);
private final BlockingDeque updates=new LinkedBlockingDeque<>();
@Override public void exec(final Mark mark) {
mark.watch((kind, target) -> updates.offer(Paths
.get("/")
.resolve(mark.target().relativize(target))
.toString()
.replace("/index.html", "/")
));
final Thread daemon=new Thread(() -> {
try {
new JSEServer()
.address(address)
.delegate(locator -> locator
.set(logger(), () -> new MavenLogger(mark.logger()))
.get(() -> new Router()
.path(ReloadQueue, new Worker()
.get(this::queue)
)
.path(ReloadScript, new Worker()
.get(this::script)
)
.path("/*", new Worker()
.get(handler(
this::rewrite,
new Publisher()
.assets(mark.target())
))
)
)
)
.start();
if ( Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE) ) {
Desktop.getDesktop().browse(URI.create(format(
"http://%s:%d/", address.getHostString(), address.getPort()
)));
}
} catch ( final IOException e ) {
throw new UncheckedIOException(e);
}
});
daemon.setDaemon(true);
daemon.start();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private Response queue(final Request request, final Function forward) {
try {
final Collection batch=new HashSet<>(Set.of(updates.take()));
updates.drainTo(batch);
return request.reply(OK)
.body(new JSON(), Json.createArrayBuilder(batch).build());
} catch ( final InterruptedException e ) {
return request.reply(OK);
}
}
private Response script(final Request request, final Function forward) {
return request.reply(OK)
.header("Content-Type", "text/javascript")
.body(new Data(), Resources.data(Serve.class, ".js"));
}
private Response rewrite(final Request request, final Function forward) {
return forward.apply(request).map(response -> {
if ( response.header("Content-Type").filter(mime -> mime.startsWith("text/html")).isPresent() ) {
final ByteArrayOutputStream buffer=new ByteArrayOutputStream(1000);
response.output().accept(buffer);
final byte[] body=buffer.toString(response.charset())
.replace("", format("%s", ReloadTag))
.getBytes(response.charset());
return response
.header("Content-Length", String.valueOf(body.length))
.output(output -> {
try {
output.write(body);
} catch ( final IOException e ) {
throw new UncheckedIOException(e);
}
});
} else {
return response;
}
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private static final class MavenLogger extends Logger {
private final Log logger;
private MavenLogger(final Log logger) {
this.logger=logger;
}
@Override public Logger entry(
final Level level, final Object source,
final Supplier message, final Throwable cause
) {
if ( cause == null ) {
final Consumer sink=(level == Level.error) ? logger::error
: (level == Level.warning) ? logger::warn
: (level == Level.info) ? logger::info
: logger::debug;
sink.accept(message.get());
} else {
final BiConsumer sink=(level == Level.error) ? logger::error
: (level == Level.warning) ? logger::warn
: (level == Level.info) ? logger::info
: logger::debug;
sink.accept(message.get(), cause);
}
return this;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy