diff --git a/bundles/org.openhab.core.io.rest.optimize/.classpath b/bundles/org.openhab.core.io.rest.optimize/.classpath new file mode 100644 index 0000000000..3c5e7d1755 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.optimize/.classpath @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.io.rest.optimize/.project b/bundles/org.openhab.core.io.rest.optimize/.project new file mode 100644 index 0000000000..42e19ac066 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.optimize/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.io.rest.optimize + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.core.io.rest.optimize/NOTICE b/bundles/org.openhab.core.io.rest.optimize/NOTICE new file mode 100644 index 0000000000..6c17d0d8a4 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.optimize/NOTICE @@ -0,0 +1,14 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-core + diff --git a/bundles/org.openhab.core.io.rest.optimize/bnd.bnd b/bundles/org.openhab.core.io.rest.optimize/bnd.bnd new file mode 100644 index 0000000000..ab247a0d37 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.optimize/bnd.bnd @@ -0,0 +1,2 @@ +Bundle-SymbolicName: ${project.artifactId} +Bundle-Activator: org.eclipse.smarthome.io.rest.optimize.internal.Activator \ No newline at end of file diff --git a/bundles/org.openhab.core.io.rest.optimize/pom.xml b/bundles/org.openhab.core.io.rest.optimize/pom.xml new file mode 100644 index 0000000000..392acc2386 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.optimize/pom.xml @@ -0,0 +1,16 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 2.5.0-SNAPSHOT + + + org.openhab.core.io.rest.optimize + + openHAB Core :: Bundles :: REST JAX-RS Optimizations + + diff --git a/bundles/org.openhab.core.io.rest.optimize/src/main/java/org/eclipse/smarthome/io/rest/optimize/internal/Activator.java b/bundles/org.openhab.core.io.rest.optimize/src/main/java/org/eclipse/smarthome/io/rest/optimize/internal/Activator.java new file mode 100644 index 0000000000..3bf7324b1f --- /dev/null +++ b/bundles/org.openhab.core.io.rest.optimize/src/main/java/org/eclipse/smarthome/io/rest/optimize/internal/Activator.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.io.rest.optimize.internal; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceRegistration; + +import com.eclipsesource.jaxrs.publisher.ResourceFilter; + +/** + * Activator + * + * It registers a {@link ResourceFilter} in order to prevent the JAX-RS implementation to + * enforce starting all services once they are registered. + * + * @author Simon Kaufmann - initial contribution and API. + * + */ +public class Activator implements BundleActivator { + + private ServiceRegistration resourceFilterRegistration; + + @Override + public void start(BundleContext context) throws Exception { + registerResourceFilter(context); + } + + @Override + public void stop(BundleContext context) throws Exception { + unregisterResourceFilter(); + } + + private void registerResourceFilter(BundleContext context) throws InvalidSyntaxException { + resourceFilterRegistration = context.registerService(ResourceFilter.class.getName(), new ResourceFilterImpl(), + null); + } + + private void unregisterResourceFilter() { + if (resourceFilterRegistration != null) { + resourceFilterRegistration.unregister(); + resourceFilterRegistration = null; + } + } + +} diff --git a/bundles/org.openhab.core.io.rest.optimize/src/main/java/org/eclipse/smarthome/io/rest/optimize/internal/ResourceFilterImpl.java b/bundles/org.openhab.core.io.rest.optimize/src/main/java/org/eclipse/smarthome/io/rest/optimize/internal/ResourceFilterImpl.java new file mode 100644 index 0000000000..8b05a466e1 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.optimize/src/main/java/org/eclipse/smarthome/io/rest/optimize/internal/ResourceFilterImpl.java @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.io.rest.optimize.internal; + +import static com.eclipsesource.jaxrs.publisher.ServiceProperties.PUBLISH; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.eclipsesource.jaxrs.publisher.ResourceFilter; + +/** + * Provides a filter for all classes/interfaces which are relevant in the context of JAX-RS. + * + * By default, this filter will allow every service outside of the org.eclipse.smarthome.**-Namespace to be parsed by + * the JAXR-RS implementation. To further optimize this, install a fragment which adds a "/res/whitelist.txt" file, + * containing one service interface or class per line like in the following example: + * + *
+ * {@code
+ * # My Custom Services
+ * org.example.foo
+ * org.example.bar
+ * # Another one
+ * org.example.test
+ * }
+ * 
+ * + * If this file is present, no other services will be scanned and hence won't be available. + * + * @author Simon Kaufmann - initial contribution and API. + * + */ +public class ResourceFilterImpl implements ResourceFilter { + + private final Logger logger = LoggerFactory.getLogger(ResourceFilterImpl.class); + + /** + * All classes and interfaces which are considered to be relevant for JAX-RS. + */ + private static final String[] WHITELIST = new String[] { + // JAX-RS + "javax.ws.rs.ext.MessageBodyReader", "javax.ws.rs.ext.MessageBodyWriter", + // openHAB + "org.eclipse.smarthome.io.rest.internal.filter.ProxyFilter", + "org.eclipse.smarthome.io.rest.internal.resources.RootResource", + "org.eclipse.smarthome.io.rest.JSONResponse$ExceptionMapper", "org.eclipse.smarthome.io.rest.RESTResource", + "org.eclipse.smarthome.io.rest.sse.internal.async.BlockingAsyncFeature", + "org.eclipse.smarthome.io.rest.sse.SseResource", + // SSE + "org.glassfish.jersey.media.sse.SseFeature", + "org.glassfish.jersey.server.monitoring.ApplicationEventListener" }; + + @Override + public Filter getFilter() { + String filterString = createFilter(WHITELIST); + try { + return FrameworkUtil.createFilter(filterString); + } catch (InvalidSyntaxException e) { + logger.error("Error creating RESTResource filter", e); + } + return null; + } + + /** + * @param interfaces interface or class names + * @return filter string which matches if the class implements one of the interfaces or the name of the class is + * contained in interfaces + */ + private String createFilter(String[] interfaces) { + StringBuilder builder = new StringBuilder(); + builder.append("(&"); + builder.append("(|"); + List whitelist = loadWhitelistExtension(); + if (whitelist == null) { + logger.debug("No /res/whitelist.txt file found - scanning all unknown services"); + builder.append("(!(" + Constants.OBJECTCLASS + "=org.eclipse.smarthome.*))"); + } else { + logger.debug("Whitelist /res/whitelist.txt file found - restricting scanning of services"); + whitelist.forEach(entry -> { + builder.append("(" + Constants.OBJECTCLASS + "=" + entry + ")"); + }); + } + for (String clazz : interfaces) { + builder.append("(" + Constants.OBJECTCLASS + "=" + clazz + ")"); + } + builder.append(")"); + builder.append("(!(" + PUBLISH + "=false)))"); + return builder.toString(); + } + + private List loadWhitelistExtension() { + Enumeration entries = FrameworkUtil.getBundle(this.getClass()).findEntries("res", "whitelist.txt", false); + if (entries != null && entries.hasMoreElements()) { + URL url = entries.nextElement(); + try (InputStream is = url.openStream()) { + return readWhitelistEntries(is); + } catch (IOException e) { + logger.warn("Error reading REST extension whitelist from {}", url, e); + return null; + } + } else { + return null; + } + } + + private List readWhitelistEntries(InputStream stream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + List ret = new LinkedList<>(); + String line = reader.readLine(); + while (line != null) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + ret.add(trimmed); + } + line = reader.readLine(); + } + return ret; + } + +}