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
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,37 @@
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);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});

browser.navigation().loadUrl("http://mydomain.com/index.html");
browser.navigation().loadUrl("https://mydomain.com/index.html");
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>This interceptor:
* <ul>
* <li>Intercepts only requests whose host matches the configured domain.</li>
* <li>Delegates the content lookup to {@link #openContent(String)}.</li>
* <li>Streams the returned content into the {@link UrlRequestJob}.</li>
* </ul>
*
* <p>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}.
*
* <p>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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.
* <p>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.
*
* <p>This interceptor responds with the following status codes:
*
* <ul>
* <li><b>200 OK</b> - the file was found and read properly.
* In this case, the {@code Content-Type} header is sent.</li>
* <li><b>404 Not Found</b> - the file could not be found due to an invalid path.</li>
* <li><b>404 Not Found</b> - the file could not be found.</li>
* <li><b>500 Internal Server Error</b> - couldn't read the file.</li>
* </ul>
*
* <p>This interceptor considers only the path component of the URL request. It ignores request
* parameters and headers.
*
* <p>Note: using this interceptor can reduce performance since it processes all incoming
* traffic under the scheme.
* <p>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<HttpHeader> 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;
}
}
Loading