diff --git a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/Application.java b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/Application.java index dfa450fc..19e69ed8 100644 --- a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/Application.java +++ b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/Application.java @@ -29,31 +29,30 @@ import com.teamdev.jxbrowser.net.Scheme; import com.teamdev.jxbrowser.view.swing.BrowserView; import java.awt.BorderLayout; -import java.nio.file.Path; import java.nio.file.Paths; import javax.swing.JFrame; import javax.swing.WindowConstants; /** - * This example demonstrates how to serve files from the folder for a certain domain. + * This example demonstrates how to serve files from the folder for + * a certain domain. */ public final class Application { public static void main(String[] args) { - Path contentRoot = Paths.get("content-root").toAbsolutePath(); - DomainToFolderInterceptor interceptor = - DomainToFolderInterceptor.create("mydomain.com", contentRoot); - EngineOptions options = + var interceptor = new DomainToFolderInterceptor("mydomain.com", + Paths.get("content-root")); + var options = EngineOptions.newBuilder(HARDWARE_ACCELERATED) - .addScheme(Scheme.HTTP, interceptor) + .addScheme(Scheme.HTTPS, interceptor) .build(); - Engine engine = Engine.newInstance(options); - Browser browser = engine.newBrowser(); + var engine = Engine.newInstance(options); + var browser = engine.newBrowser(); invokeLater(() -> { - BrowserView view = BrowserView.newInstance(browser); + var view = BrowserView.newInstance(browser); - JFrame frame = new JFrame("Serve Files from Folder"); + var frame = new JFrame("Serve Files from Folder"); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.add(view, BorderLayout.CENTER); frame.setSize(1280, 720); @@ -61,6 +60,6 @@ public static void main(String[] args) { frame.setVisible(true); }); - browser.navigation().loadUrl("http://mydomain.com/index.html"); + browser.navigation().loadUrl("https://mydomain.com/index.html"); } } diff --git a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainContentInterceptor.java b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainContentInterceptor.java new file mode 100644 index 00000000..41869f6d --- /dev/null +++ b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainContentInterceptor.java @@ -0,0 +1,115 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.teamdev.jxbrowser.examples.interceptor; + +import static com.teamdev.jxbrowser.net.HttpStatus.INTERNAL_SERVER_ERROR; +import static com.teamdev.jxbrowser.net.HttpStatus.NOT_FOUND; +import static com.teamdev.jxbrowser.net.HttpStatus.OK; + +import com.teamdev.jxbrowser.net.HttpHeader; +import com.teamdev.jxbrowser.net.HttpStatus; +import com.teamdev.jxbrowser.net.UrlRequestJob; +import com.teamdev.jxbrowser.net.callback.InterceptUrlRequestCallback; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +/** + * A base URL interceptor that serves content for a specific domain by + * delegating the actual content lookup to subclasses. + * + *

This interceptor: + *

+ * + *

Subclasses need to provide the logic that locates the content (for example, + * on disk or on the classpath) and returns it as an {@link InputStream}. + * The MIME type is derived from the requested path by this base class. + */ +abstract class DomainContentInterceptor implements InterceptUrlRequestCallback { + + private static final String CONTENT_TYPE = "Content-Type"; + private final String domain; + + DomainContentInterceptor(String domain) { + this.domain = domain; + } + + @Override + public Response on(Params params) { + var uri = URI.create(params.urlRequest().url()); + if (!uri.getHost().equals(domain)) { + // Let Chromium process requests to other domains as usual. + return Response.proceed(); + } + var path = uri.getPath().substring(1); + try (var content = openContent(path)) { + if (content == null) { + var job = createJob(params, NOT_FOUND); + job.complete(); + return Response.intercept(job); + } + var mimeType = MimeTypes.mimeType(path); + var contentType = HttpHeader.of(CONTENT_TYPE, mimeType); + var job = createJob(params, OK, contentType); + writeToJob(content, job); + job.complete(); + return Response.intercept(job); + } catch (IOException e) { + // Return 500 response when the file read failed. + var job = createJob(params, INTERNAL_SERVER_ERROR); + job.complete(); + return Response.intercept(job); + } catch (Exception e) { + return Response.proceed(); + } + } + + /** + * Locates the content for the given {@code uri}. + * + *

If the content cannot be found, this method should return + * {@code null}. If the content cannot be read, it should throw an + * {@link IOException}. + */ + protected abstract InputStream openContent(String path) throws IOException; + + private UrlRequestJob createJob(Params params, HttpStatus status, + HttpHeader... headers) { + var options = UrlRequestJob.Options.newBuilder(status); + for (var header : headers) { + options.addHttpHeader(header); + } + return params.newUrlRequestJob(options.build()); + } + + /** + * Writes content of the input stream into the HTTP response. + */ + private void writeToJob(InputStream stream, UrlRequestJob job) + throws IOException { + var content = stream.readAllBytes(); + job.write(content); + } +} diff --git a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainToFolderInterceptor.java b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainToFolderInterceptor.java index 8b189ada..5ed5419d 100644 --- a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainToFolderInterceptor.java +++ b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainToFolderInterceptor.java @@ -20,139 +20,62 @@ package com.teamdev.jxbrowser.examples.interceptor; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.teamdev.jxbrowser.examples.interceptor.MimeTypes.mimeType; -import static com.teamdev.jxbrowser.internal.string.StringPreconditions.checkNotNullEmptyOrBlank; -import static com.teamdev.jxbrowser.logging.Logger.error; -import static com.teamdev.jxbrowser.net.HttpStatus.INTERNAL_SERVER_ERROR; -import static com.teamdev.jxbrowser.net.HttpStatus.NOT_FOUND; -import static com.teamdev.jxbrowser.net.HttpStatus.OK; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; +import static java.nio.file.Files.exists; +import static java.nio.file.Files.isDirectory; -import com.teamdev.jxbrowser.net.HttpHeader; -import com.teamdev.jxbrowser.net.HttpStatus; -import com.teamdev.jxbrowser.net.UrlRequestJob; -import com.teamdev.jxbrowser.net.callback.InterceptUrlRequestCallback; import java.io.FileInputStream; import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; +import java.io.InputStream; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; /** - * An interceptor that treats every URL under the given domain as a path to the file on disk and - * loads it. + * An interceptor that treats every URL under the given domain as a path to a + * file on disk and loads it. * - *

The interceptor is configured with the domain name and the content directory. For every - * request, it takes the path component of the URL and looks for it in the content directory. That - * means a request to {@code example.com/docs/index.html} will load {@code docs/index.html} file - * from the content directory. The mime type of the file is derived automatically. + *

The interceptor is configured with the domain name and the content + * directory. For every request, it takes the path component of the URL and + * looks for it in the content directory. That means a request to + * {@code example.com/docs/index.html} will load {@code docs/index.html} file + * from the content directory. The MIME type of the file is derived + * automatically. * *

This interceptor responds with the following status codes: * *

* - *

This interceptor considers only the path component of the URL request. It ignores request - * parameters and headers. - * - *

Note: using this interceptor can reduce performance since it processes all incoming - * traffic under the scheme. + *

This interceptor considers only the path component of the URL request. + * It ignores request parameters and headers. */ -public final class DomainToFolderInterceptor implements InterceptUrlRequestCallback { - - private static final String CONTENT_TYPE = "Content-Type"; +public final class DomainToFolderInterceptor extends DomainContentInterceptor { - private final String domain; private final Path contentRoot; - private DomainToFolderInterceptor(String domain, Path contentRoot) { - this.domain = domain; - this.contentRoot = contentRoot; - } - /** - * Creates a URL interceptor for the given domain to load files from the given directory. + * Creates a URL interceptor for the given domain to load files from the + * given directory. * * @param domain a domain name to intercept * @param contentRoot a path to the directory with files to load */ - public static DomainToFolderInterceptor create(String domain, Path contentRoot) { - checkNotNull(contentRoot); - checkNotNullEmptyOrBlank(domain); - return new DomainToFolderInterceptor(domain, contentRoot.toAbsolutePath()); + public DomainToFolderInterceptor(String domain, Path contentRoot) { + super(domain); + this.contentRoot = contentRoot.toAbsolutePath(); } + /** + * Resolves the requested path to a file and opens it. + */ @Override - public Response on(Params params) { - URI uri = URI.create(params.urlRequest().url()); - if (shouldNotBeIntercepted(uri)) { - return Response.proceed(); - } - Path filePath = getPathOnDisk(uri); - UrlRequestJob job; - if (fileExists(filePath)) { - HttpHeader contentType = getContentType(filePath); - job = createJob(params, OK, singletonList(contentType)); - try { - readFile(filePath, job); - } catch (IOException e) { - error("Failed to read file {0}", e, filePath); - job = createJob(params, INTERNAL_SERVER_ERROR); - } - } else { - job = createJob(params, NOT_FOUND); - } - job.complete(); - return Response.intercept(job); - } - - private boolean shouldNotBeIntercepted(URI uri) { - return !uri.getHost().equals(domain); - } - - private Path getPathOnDisk(URI uri) { - return Paths.get(contentRoot.toString(), uri.getPath()); - } - - private boolean fileExists(Path filePath) { - return Files.exists(filePath) && !Files.isDirectory(filePath); - } - - private HttpHeader getContentType(Path file) { - return HttpHeader.of(CONTENT_TYPE, mimeType(file).value()); - } - - private UrlRequestJob createJob(Params params, - HttpStatus httpStatus, - List httpHeaders) { - UrlRequestJob.Options.Builder builder = UrlRequestJob.Options.newBuilder(httpStatus); - httpHeaders.forEach(builder::addHttpHeader); - return params.newUrlRequestJob(builder.build()); - } - - private UrlRequestJob createJob(Params params, HttpStatus httpStatus) { - return createJob(params, httpStatus, emptyList()); - } - - private void readFile(Path filePath, UrlRequestJob job) throws IOException { - try (FileInputStream stream = new FileInputStream(filePath.toFile())) { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = stream.read(buffer)) > 0) { - if (bytesRead != buffer.length) { - buffer = Arrays.copyOf(buffer, bytesRead); - } - job.write(buffer); - } + protected InputStream openContent(String path) throws IOException { + var filePath = contentRoot.resolve(path); + if (exists(filePath) && !isDirectory(filePath)) { + return new FileInputStream(filePath.toFile()); } + return null; } } diff --git a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainToResourceInterceptor.java b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainToResourceInterceptor.java new file mode 100644 index 00000000..ef982ded --- /dev/null +++ b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/DomainToResourceInterceptor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.teamdev.jxbrowser.examples.interceptor; + +import java.io.InputStream; + +/** + * An interceptor that treats every URL under the given domain as a path to a + * resources in the classpath and loads it. + * + *

The interceptor is configured with the domain name and the content + * root path. For every request, it takes the path component of the URL and + * looks for it in the classpath. That means a request to + * {@code example.com/docs/index.html} will load {@code docs/index.html} file + * from the resources. The MIME type of the file is derived + * automatically. + * + *

This interceptor responds with the following status codes: + * + *

+ * + *

This interceptor considers only the path component of the URL request. + * It ignores request parameters and headers. + */ + +public final class DomainToResourceInterceptor extends + DomainContentInterceptor { + + private final String resourceRoot; + + /** + * Creates a URL interceptor for the given domain to load files from the + * given classpath root. + * + * @param domain a domain name to intercept + * @param resourceRoot a root path on the classpath to look up resources + * under + */ + public DomainToResourceInterceptor(String domain, String resourceRoot) { + super(domain); + this.resourceRoot = resourceRoot; + } + + protected InputStream openContent(String path) { + var resourcePath = toResourcePath(path); + return getClass().getClassLoader().getResourceAsStream(resourcePath); + } + + private String toResourcePath(String uriPath) { + var path = uriPath.startsWith("/") ? uriPath.substring(1) : uriPath; + if (resourceRoot.isEmpty()) { + return path; + } + if (resourceRoot.endsWith("/")) { + return resourceRoot + path; + } + return resourceRoot + "/" + path; + } +} diff --git a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/MimeTypes.java b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/MimeTypes.java index 7772db29..83079de3 100644 --- a/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/MimeTypes.java +++ b/tutorials/serve-from-directory/src/main/java/com/teamdev/jxbrowser/examples/interceptor/MimeTypes.java @@ -21,26 +21,19 @@ package com.teamdev.jxbrowser.examples.interceptor; import static com.teamdev.jxbrowser.logging.Logger.warn; -import static java.lang.String.format; +import static java.util.Locale.ROOT; -import com.google.common.collect.ImmutableMap; -import com.teamdev.jxbrowser.internal.Lazy; -import com.teamdev.jxbrowser.net.MimeType; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Path; -import java.util.Locale; -import java.util.Map; import java.util.Properties; /** * A utility for working with MIME types. */ -public final class MimeTypes { +final class MimeTypes { - private static final MimeType OCTET_STREAM = MimeType.of("application/octet-stream"); - private static final Lazy> extToMime = new Lazy<>(MimeTypes::createMap); + private static final String OCTET_STREAM = "application/octet-stream"; + private static final String MIME_TYPES_FILE = "ext-to-mime.properties"; + private static final Properties MIME_TYPES = MimeTypes.createMap(); /** * Prevents instantiation of this utility class. @@ -49,31 +42,30 @@ private MimeTypes() { } /** - * Derives {@code MimeType} from the extension of the {@code file}. + * Derives {@code MimeType} from the extension of the {@code fileName}. * - *

If the file extension is not recognized, {@link #OCTET_STREAM} is returned. + *

If the file extension is not recognized, {@link #OCTET_STREAM} is + * returned. * - * @param file the path to the file + * @param fileName the file name */ - public static MimeType mimeType(Path file) { - String fileName = file.getFileName().toString(); - String extension = fileName.substring(fileName.lastIndexOf(".") + 1); - return extToMime.get().getOrDefault(extension.toLowerCase(Locale.ENGLISH), OCTET_STREAM); + static String mimeType(String fileName) { + var extension = fileName.substring(fileName.lastIndexOf(".") + 1) + .toLowerCase(ROOT); + var type = MIME_TYPES.getOrDefault(extension, OCTET_STREAM); + return type.toString(); } - private static Map createMap() { - ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); - Properties properties = new Properties(); - URL propertiesUrl = MimeTypes.class.getClassLoader().getResource("ext-to-mime.properties"); - if (propertiesUrl != null) { - try (InputStream inputStream = propertiesUrl.openStream()) { + private static Properties createMap() { + var properties = new Properties(); + var url = MimeTypes.class.getClassLoader().getResource(MIME_TYPES_FILE); + if (url != null) { + try (var inputStream = url.openStream()) { properties.load(inputStream); - properties.forEach((key, value) -> mapBuilder.put(key.toString(), - MimeType.of(value.toString()))); } catch (IOException ignore) { - warn(format("Couldn't read the list of mime-types from: %s", propertiesUrl)); + warn("Couldn't read the list of mime-types"); } } - return mapBuilder.build(); + return properties; } }