From: Mathieu Baudier Date: Mon, 23 Sep 2024 13:08:49 +0000 (+0200) Subject: Use HTTP handler for package resources publication X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=2326f1337591da5f183a5e5785844d7fb5c274b5;p=lgpl%2Fargeo-commons.git Use HTTP handler for package resources publication --- diff --git a/org.argeo.cms.ee/OSGI-INF/pkgServlet.xml b/org.argeo.cms.ee/OSGI-INF/pkgServlet.xml deleted file mode 100644 index 05978e669..000000000 --- a/org.argeo.cms.ee/OSGI-INF/pkgServlet.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/org.argeo.cms.ee/bnd.bnd b/org.argeo.cms.ee/bnd.bnd index 592abffdb..83df366bb 100644 --- a/org.argeo.cms.ee/bnd.bnd +++ b/org.argeo.cms.ee/bnd.bnd @@ -10,6 +10,4 @@ org.argeo.cms.osgi,\ Service-Component:\ -OSGI-INF/pkgServletContext.xml,\ -OSGI-INF/pkgServlet.xml,\ OSGI-INF/statusHandler.xml,\ diff --git a/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java deleted file mode 100644 index 07e3ccca0..000000000 --- a/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.argeo.cms.servlet.internal; - -import java.io.IOException; -import java.io.InputStream; -import java.net.FileNameMap; -import java.net.URL; -import java.net.URLConnection; -import java.util.Collection; -import java.util.SortedMap; -import java.util.TreeMap; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.argeo.cms.osgi.FilterRequirement; -import org.argeo.cms.osgi.PublishNamespace; -import org.argeo.cms.util.StreamUtils; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.Version; -import org.osgi.framework.VersionRange; -import org.osgi.framework.namespace.PackageNamespace; -import org.osgi.framework.wiring.BundleCapability; -import org.osgi.framework.wiring.BundleWiring; -import org.osgi.framework.wiring.FrameworkWiring; -import org.osgi.resource.Requirement; - -/** - * Publishes client-side web resources (JavaScript, HTML, CSS, images, etc.) - * from the OSGi runtime. - */ -public class PkgServlet extends HttpServlet { - private static final long serialVersionUID = 7660824185145214324L; - - private static FileNameMap fileNameMap = URLConnection.getFileNameMap(); - - private BundleContext bundleContext = FrameworkUtil.getBundle(PkgServlet.class).getBundleContext(); - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String pathInfo = req.getPathInfo(); - - String pkg, versionStr, file; - String[] parts = pathInfo.split("/"); - // first is always empty - if (parts.length == 4) { - pkg = parts[1]; - versionStr = parts[2]; - file = parts[3]; - } else if (parts.length == 3) { - pkg = parts[1]; - versionStr = null; - file = parts[2]; - } else { - throw new IllegalArgumentException("Unsupported path length " + pathInfo); - } - - // content type - String contentType = fileNameMap.getContentTypeFor(file); - resp.setContentType(contentType); - - FrameworkWiring frameworkWiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class); - String filter; - if (versionStr == null) { - filter = "(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")"; - } else { - if (versionStr.startsWith("[") || versionStr.startsWith("(")) {// range - VersionRange versionRange = new VersionRange(versionStr); - filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")" - + versionRange.toFilterString(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE) + ")"; - - } else { - Version version = new Version(versionStr); - filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")(" - + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + version + "))"; - } - } - Requirement requirement = new FilterRequirement(PackageNamespace.PACKAGE_NAMESPACE, filter); - Collection packages = frameworkWiring.findProviders(requirement); - if (packages.isEmpty()) { - resp.sendError(404); - return; - } - - // TODO verify that it works with multiple versions - SortedMap sorted = new TreeMap<>(); - for (BundleCapability capability : packages) { - sorted.put(capability.getRevision().getVersion(), capability); - } - - Bundle bundle = sorted.get(sorted.firstKey()).getRevision().getBundle(); - String entryPath = '/' + pkg.replace('.', '/') + '/' + file; - URL internalURL = bundle.getResource(entryPath); - if (internalURL == null) { - resp.sendError(404); - return; - } - - // Resource found, we now check whether it can be published - boolean publish = false; - BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); - capabilities: for (BundleCapability bundleCapability : bundleWiring - .getCapabilities(PublishNamespace.CMS_PUBLISH_NAMESPACE)) { - Object publishedPkg = bundleCapability.getAttributes().get(PublishNamespace.PKG); - if (publishedPkg != null) { - if (publishedPkg.equals("*") || publishedPkg.equals(pkg)) { - Object publishedFile = bundleCapability.getAttributes().get(PublishNamespace.FILE); - if (publishedFile == null) { - publish = true; - break capabilities; - } else { - String[] publishedFiles = publishedFile.toString().split(","); - for (String pattern : publishedFiles) { - if (pattern.startsWith("*.")) { - String ext = pattern.substring(1); - if (file.endsWith(ext)) { - publish = true; - break capabilities; - } - } else { - if (publishedFile.equals(file)) { - publish = true; - break capabilities; - } - } - } - } - } - } - } - - if (!publish) { - resp.sendError(404); - return; - } - - try (InputStream in = internalURL.openStream()) { - StreamUtils.copy(in, resp.getOutputStream()); - } - } - -} diff --git a/org.argeo.cms/OSGI-INF/pkgHttpHandler.xml b/org.argeo.cms/OSGI-INF/pkgHttpHandler.xml new file mode 100644 index 000000000..4afd9a65e --- /dev/null +++ b/org.argeo.cms/OSGI-INF/pkgHttpHandler.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd index bcba7939e..3fd92c226 100644 --- a/org.argeo.cms/bnd.bnd +++ b/org.argeo.cms/bnd.bnd @@ -1,6 +1,7 @@ Bundle-Activator: org.argeo.cms.internal.osgi.CmsActivator Import-Package: \ +org.osgi.framework.namespace,\ * Export-Package:\ @@ -15,8 +16,9 @@ OSGI-INF/transactionManager.xml,\ OSGI-INF/cmsUserAdmin.xml,\ OSGI-INF/cmsUserManager.xml,\ OSGI-INF/cmsContentRepository.xml,\ -OSGI-INF/cmsAcrHttpHandler.xml,\ OSGI-INF/cmsDeployment.xml,\ OSGI-INF/cmsContext.xml,\ OSGI-INF/cmsFileSystemProvider.xml,\ +OSGI-INF/cmsAcrHttpHandler.xml,\ +OSGI-INF/pkgHttpHandler.xml,\ diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java b/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java index 3b9a47a38..f899bdcee 100644 --- a/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java +++ b/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java @@ -7,15 +7,23 @@ package org.argeo.cms.http; */ public enum HttpStatus { // Successful responses (200–299) + /** 200 */ OK(200, "OK"), // + /** 204 */ NO_CONTENT(204, "No Content"), // + /** 207 */ MULTI_STATUS(207, "Multi-Status"), // WebDav // Client error responses (400–499) + /** 401 */ UNAUTHORIZED(401, "Unauthorized"), // + /** 403 */ FORBIDDEN(403, "Forbidden"), // + /** 404 */ NOT_FOUND(404, "Not Found"), // // Server error responses (500-599) + /** 500 */ INTERNAL_SERVER_ERROR(500, "Internal Server Error"), // + /** 501 */ NOT_IMPLEMENTED(501, "Not Implemented"), // ; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/PkgHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/internal/http/PkgHttpHandler.java new file mode 100644 index 000000000..a737ab672 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/PkgHttpHandler.java @@ -0,0 +1,163 @@ +package org.argeo.cms.internal.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.FileNameMap; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collection; +import java.util.Collections; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.argeo.cms.http.HttpHeader; +import org.argeo.cms.http.HttpStatus; +import org.argeo.cms.http.server.HttpServerUtils; +import org.argeo.cms.osgi.FilterRequirement; +import org.argeo.cms.osgi.PublishNamespace; +import org.argeo.cms.util.StreamUtils; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.resource.Requirement; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +/** + * Publishes client-side web resources (JavaScript, HTML, CSS, images, etc.) + * from the OSGi runtime. + */ +public class PkgHttpHandler implements HttpHandler { + // TODO make it configurable? + private FileNameMap fileNameMap = URLConnection.getFileNameMap(); + + private FrameworkWiring frameworkWiring = null; + + public void start(BundleContext bundleContext) { + this.frameworkWiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class); + } + + public void stop() { + this.frameworkWiring = null; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + // String pathInfo = req.getPathInfo(); + String pathInfo = HttpServerUtils.subPath(exchange); + + String pkg, versionStr, file; + String[] parts = pathInfo.split("/"); + // first is always empty + if (parts.length == 4) { + pkg = parts[1]; + versionStr = parts[2]; + file = parts[3]; + } else if (parts.length == 3) { + pkg = parts[1]; + versionStr = null; + file = parts[2]; + } else { + throw new IllegalArgumentException("Unsupported path length " + pathInfo); + } + + // content type + String contentType = fileNameMap.getContentTypeFor(file); + exchange.getResponseHeaders().put(HttpHeader.CONTENT_TYPE.getHeaderName(), + Collections.singletonList(contentType)); +// resp.setContentType(contentType); + + String filter; + if (versionStr == null) { + filter = "(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")"; + } else { + if (versionStr.startsWith("[") || versionStr.startsWith("(")) {// range + VersionRange versionRange = new VersionRange(versionStr); + filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")" + + versionRange.toFilterString(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE) + ")"; + + } else { + Version version = new Version(versionStr); + filter = "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + pkg + ")(" + + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + version + "))"; + } + } + Requirement requirement = new FilterRequirement(PackageNamespace.PACKAGE_NAMESPACE, filter); + Collection packages = frameworkWiring.findProviders(requirement); + if (packages.isEmpty()) { +// resp.sendError(404); + exchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1); + return; + } + + // TODO verify that it works with multiple versions + SortedMap sorted = new TreeMap<>(); + for (BundleCapability capability : packages) { + sorted.put(capability.getRevision().getVersion(), capability); + } + + Bundle bundle = sorted.get(sorted.firstKey()).getRevision().getBundle(); + String entryPath = '/' + pkg.replace('.', '/') + '/' + file; + URL internalURL = bundle.getResource(entryPath); + if (internalURL == null) { +// resp.sendError(404); + exchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1); + return; + } + + // Resource found, we now check whether it can be published + boolean publish = false; + BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); + capabilities: for (BundleCapability bundleCapability : bundleWiring + .getCapabilities(PublishNamespace.CMS_PUBLISH_NAMESPACE)) { + Object publishedPkg = bundleCapability.getAttributes().get(PublishNamespace.PKG); + if (publishedPkg != null) { + if (publishedPkg.equals("*") || publishedPkg.equals(pkg)) { + Object publishedFile = bundleCapability.getAttributes().get(PublishNamespace.FILE); + if (publishedFile == null) { + publish = true; + break capabilities; + } else { + String[] publishedFiles = publishedFile.toString().split(","); + for (String pattern : publishedFiles) { + if (pattern.startsWith("*.")) { + String ext = pattern.substring(1); + if (file.endsWith(ext)) { + publish = true; + break capabilities; + } + } else { + if (publishedFile.equals(file)) { + publish = true; + break capabilities; + } + } + } + } + } + } + } + + if (!publish) { +// resp.sendError(404); + exchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1); + return; + } + + // TODO cache length? + exchange.sendResponseHeaders(HttpStatus.OK.getCode(), 0); + try (InputStream in = internalURL.openStream(); // + OutputStream out = exchange.getResponseBody();) { +// StreamUtils.copy(in, resp.getOutputStream()); + StreamUtils.copy(in, out); + } + } + +}