You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2020/01/10 15:22:35 UTC
[isis] 21/21: Merge remote-tracking branch 'origin/master' into ide
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch ISIS-2259
in repository https://gitbox.apache.org/repos/asf/isis.git
commit 868a69c055362ff830e4a02444bad00955ab4a30
Merge: 31b8b21 ba33cc2
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Jan 10 15:21:34 2020 +0000
Merge remote-tracking branch 'origin/master' into ide
# Conflicts:
# core/config/src/main/java/org/apache/isis/config/viewer/wicket/WebAppContextPath.java
# core/security/shiro/src/main/java/org/apache/isis/security/shiro/webmodule/WebModuleShiro.java
# core/viewers/restfulobjects/jaxrs-resteasy-4/src/main/java/org/apache/isis/viewer/restfulobjects/jaxrsresteasy4/webmodule/WebModuleJaxrsResteasy4.java
# core/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/webmodule/IsisRestfulObjectsSessionFilter.java
# core/webapp/src/main/java/org/apache/isis/webapp/modules/templresources/TemplateResourceServlet.java
# valuetypes/sse/src/main/java/org/apache/isis/extensions/sse/webmodule/ServerSentEventsServlet.java
.../isis/core/config/viewer/wicket/WebAppContextPath.java | 2 ++
.../modules/templresources/TemplateResourceServlet.java | 9 ++++-----
.../webapp/webappctx/IsisWebAppContextInitializer.java | 15 ++++++++++++++-
.../runtime/jee_support/IsisCDIBeanScanInterceptor.java | 2 ++
.../isis/security/shiro/webmodule/WebModuleShiro.java | 10 ++++++++++
.../sse/ui/webmodule/ServerSentEventsServlet.java | 6 +++---
.../jaxrsresteasy4/webmodule/WebModuleJaxrsResteasy4.java | 14 ++++++++++++++
.../viewer/webmodule/IsisRestfulObjectsSessionFilter.java | 8 ++++----
.../webmodule/IsisTransactionFilterForRestfulObjects.java | 6 +++---
9 files changed, 56 insertions(+), 16 deletions(-)
diff --cc core/config/src/main/java/org/apache/isis/core/config/viewer/wicket/WebAppContextPath.java
index 6130055,0000000..de8f391
mode 100644,000000..100644
--- a/core/config/src/main/java/org/apache/isis/core/config/viewer/wicket/WebAppContextPath.java
+++ b/core/config/src/main/java/org/apache/isis/core/config/viewer/wicket/WebAppContextPath.java
@@@ -1,100 -1,0 +1,102 @@@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.isis.core.config.viewer.wicket;
+
+import java.io.Serializable;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.core.commons.internal.base._Strings;
+import org.apache.isis.core.commons.internal.resources._Resources;
+
+import lombok.Getter;
+
+/**
+ * This class is {@link Serializable} so that it can be injected into Wicket components.
+ */
+@Service
+@Singleton
+@Named("isisConfig.WebAppContextPath")
+@Order(OrderPrecedence.MIDPOINT)
+@Primary
+@Qualifier("Default")
+public class WebAppContextPath implements Serializable {
+
++ private static final long serialVersionUID = 1L;
++
+ /**
+ * In the form "xxx/yyy" (no leading nor trailing '/').
+ */
+ @Getter
+ private String contextPath = "";
+ private static final Pattern pattern = Pattern.compile("^[/]*(.+?)[/]*$");;
+
+ public void setContextPath(final String contextPath) {
+ if(contextPath == null) {
+ this.contextPath = "";
+ return;
+ }
+ final Matcher matcher = pattern.matcher(contextPath);
+ this.contextPath = matcher.matches()
+ ? matcher.group(1)
+ : "";
+ }
+
+ public final String prependContextPath(String path) {
+
+ if(path==null) {
+ return getContextPath();
+ }
+
+ final String contextPath = getContextPath();
+ if(_Strings.isNullOrEmpty(contextPath)) {
+ return path;
+ }
+
+ return contextPath + prefixed(path, "/");
+ }
+
+ private static String prefixed(final String path, final String prefix) {
+ return !path.startsWith(prefix)
+ ? prefix + path
+ : path;
+ }
+
+ public String prependContextPathIfLocal(String url) {
+
+ if(url==null) {
+ return null;
+ }
+ if(_Resources.isLocalResource(url)) {
+ return this.prependContextPath(url);
+ }
+ return url;
+ }
+
+
+}
diff --cc core/webapp/src/main/java/org/apache/isis/webapp/modules/templresources/TemplateResourceServlet.java
index b6f1c92,0a35c02..e79aaa3
--- a/core/webapp/src/main/java/org/apache/isis/webapp/modules/templresources/TemplateResourceServlet.java
+++ b/core/webapp/src/main/java/org/apache/isis/webapp/modules/templresources/TemplateResourceServlet.java
@@@ -30,16 -29,18 +29,18 @@@ import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+ import org.springframework.beans.factory.annotation.Autowired;
+
-import org.apache.isis.commons.internal.base._Bytes;
-import org.apache.isis.commons.internal.base._Strings;
-import org.apache.isis.config.RestEasyConfiguration;
-import org.apache.isis.config.viewer.wicket.WebAppContextPath;
-import org.apache.isis.metamodel.commons.InputStreamExtensions;
-import org.apache.isis.metamodel.commons.ResourceUtil;
-import org.apache.isis.metamodel.commons.StringExtensions;
-
-import static org.apache.isis.commons.internal.base._Strings.pair;
-import static org.apache.isis.commons.internal.base._With.ifPresentElseGet;
+import org.apache.isis.core.commons.internal.base._Bytes;
+import org.apache.isis.core.commons.internal.base._Strings;
+import org.apache.isis.core.config.RestEasyConfiguration;
+import org.apache.isis.core.config.viewer.wicket.WebAppContextPath;
+import org.apache.isis.core.metamodel.commons.InputStreamExtensions;
+import org.apache.isis.core.metamodel.commons.ResourceUtil;
+import org.apache.isis.core.metamodel.commons.StringExtensions;
+
+import static org.apache.isis.core.commons.internal.base._Strings.pair;
+import static org.apache.isis.core.commons.internal.base._With.ifPresentElseGet;
import lombok.val;
import lombok.extern.log4j.Log4j2;
diff --cc core/webapp/src/main/java/org/apache/isis/webapp/webappctx/IsisWebAppContextInitializer.java
index 7d85cc7,2a9507b..ce8a93b
--- a/core/webapp/src/main/java/org/apache/isis/webapp/webappctx/IsisWebAppContextInitializer.java
+++ b/core/webapp/src/main/java/org/apache/isis/webapp/webappctx/IsisWebAppContextInitializer.java
@@@ -27,11 -28,12 +28,12 @@@ import javax.servlet.ServletContextList
import javax.servlet.ServletException;
import org.springframework.boot.web.servlet.ServletContextInitializer;
+ import org.springframework.stereotype.Component;
import org.apache.isis.applib.services.registry.ServiceRegistry;
-import org.apache.isis.commons.internal.context._Context;
-import org.apache.isis.config.IsisConfiguration;
-import org.apache.isis.config.viewer.wicket.WebAppContextPath;
+import org.apache.isis.core.commons.internal.context._Context;
+import org.apache.isis.core.config.IsisConfiguration;
+import org.apache.isis.core.config.viewer.wicket.WebAppContextPath;
import org.apache.isis.webapp.modules.WebModule;
import org.apache.isis.webapp.modules.WebModuleContext;
diff --cc security/shiro/src/main/java/org/apache/isis/security/shiro/webmodule/WebModuleShiro.java
index 35ab433,0000000..c194969
mode 100644,000000..100644
--- a/security/shiro/src/main/java/org/apache/isis/security/shiro/webmodule/WebModuleShiro.java
+++ b/security/shiro/src/main/java/org/apache/isis/security/shiro/webmodule/WebModuleShiro.java
@@@ -1,192 -1,0 +1,202 @@@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.isis.security.shiro.webmodule;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.servlet.ServletContext;
++import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletException;
+
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.realm.Realm;
+import org.apache.shiro.web.env.EnvironmentLoaderListener;
+import org.apache.shiro.web.env.IniWebEnvironment;
+import org.apache.shiro.web.env.WebEnvironment;
+import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
+import org.apache.shiro.web.servlet.ShiroFilter;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+import org.springframework.util.ReflectionUtils;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.applib.services.inject.ServiceInjector;
+import org.apache.isis.core.commons.collections.Can;
+import org.apache.isis.core.commons.internal._Constants;
+import org.apache.isis.core.commons.internal.base._Strings;
++import org.apache.isis.core.commons.internal.exceptions._Exceptions;
+import org.apache.isis.webapp.modules.WebModuleAbstract;
+import org.apache.isis.webapp.modules.WebModuleContext;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.val;
+
+/**
+ * WebModule to enable support for Shiro.
+ * <p>
+ * Can be customized via static {@link WebModuleShiro#setShiroEnvironmentClass(Class)}
+ * @since 2.0
+ */
+@Service
+@Named("isisSecurityKeycloak.WebModuleKeycloak")
+@Order(OrderPrecedence.HIGHEST + 200)
+@Qualifier("Shiro")
+public class WebModuleShiro extends WebModuleAbstract {
+
+
+ private final static String SHIRO_FILTER_NAME = "ShiroFilter";
+
+ @Inject
+ public WebModuleShiro(final ServiceInjector serviceInjector) {
+ super(serviceInjector);
+ }
+
+ // -- CONFIGURATION
+
+ public static void setShiroEnvironmentClass(Class<? extends WebEnvironment> shiroEnvironmentClass) {
+ if(shiroEnvironmentClass==null) {
+ System.setProperty("shiroEnvironmentClass", null);
+ return;
+ }
+ System.setProperty("shiroEnvironmentClass", shiroEnvironmentClass.getName());
+ }
+
+ public static class IniWebEnvironmentUsingSystemProperty extends IniWebEnvironment {
+ @Override
+ public Ini getIni() {
+ val customShiroIniResource = System.getProperty("shiroIniResource");
+ if(_Strings.isNotEmpty(customShiroIniResource)) {
+ val ini = new Ini();
+ ini.loadFromPath(customShiroIniResource);
+ return ini;
+ }
+ return null;
+ }
+ // see https://issues.apache.org/jira/browse/SHIRO-610
+ @Override
+ protected Map<String, Object> getDefaults() {
+ Map<String, Object> defaults = new HashMap<String, Object>();
+ defaults.put(FILTER_CHAIN_RESOLVER_NAME, new PathMatchingFilterChainResolver());
+ return defaults;
+ }
+ }
+
+ public static void setShiroIniResource(String resourcePath) {
+ if(resourcePath==null) {
+ System.setProperty("shiroIniResource", null);
+ setShiroEnvironmentClass(null);
+ return;
+ }
+ System.setProperty("shiroIniResource", resourcePath);
+ setShiroEnvironmentClass(IniWebEnvironmentUsingSystemProperty.class);
+ }
+
+ /**
+ * Adds support for dependency injection into security realms
+ * @since 2.0
+ */
+ @NoArgsConstructor // don't remove, this is class is managed by Isis
+ public static class EnvironmentLoaderListenerForIsis extends EnvironmentLoaderListener {
+
+ @Inject private ServiceInjector serviceInjector;
+
+ // testing support
+ public EnvironmentLoaderListenerForIsis(ServiceInjector serviceInjector) {
+ this.serviceInjector = serviceInjector;
+ }
+
++ @Override
++ public void contextInitialized(ServletContextEvent sce) {
++ System.err.println("intercepted: " + this);
++ _Exceptions.dumpStackTrace();
++ System.err.println("======================================================");
++ super.contextInitialized(sce);
++ }
++
+ @Override
+ protected WebEnvironment createEnvironment(ServletContext servletContext) {
+ val shiroEnvironment = super.createEnvironment(servletContext);
+ val securityManager = shiroEnvironment.getSecurityManager();
+
+ injectServicesIntoRealms(securityManager);
+
+ return shiroEnvironment;
+ }
+
+ @SuppressWarnings("unchecked")
+ @SneakyThrows
+ public void injectServicesIntoRealms(
+ org.apache.shiro.mgt.SecurityManager securityManager) {
+
+ // reflective access to SecurityManager.getRealms()
+ val realms = (Collection<Realm>) ReflectionUtils
+ .findMethod(securityManager.getClass(), "getRealms")
+ .invoke(securityManager, _Constants.emptyObjects);
+
+ realms.stream().forEach(serviceInjector::injectServicesInto);
+ }
+
+ }
+
+ // --
+
+ @Getter
+ private final String name = "Shiro";
+
+ @Override
+ public void prepare(final WebModuleContext ctx) {
+ super.prepare(ctx);
+ val customShiroEnvironmentClassName = System.getProperty("shiroEnvironmentClass");
+ if(_Strings.isEmpty(customShiroEnvironmentClassName)) {
+ setShiroEnvironmentClass(IniWebEnvironmentUsingSystemProperty.class);
+ }
+ }
+
+ @Override
+ public Can<ServletContextListener> init(ServletContext ctx) throws ServletException {
+
+ registerFilter(ctx, SHIRO_FILTER_NAME, ShiroFilter.class)
+ .ifPresent(filterReg -> {
+ filterReg.addMappingForUrlPatterns(
+ null,
+ false, // filter is forced first
+ "/*");
+ });
+
+ val customShiroEnvironmentClassName = System.getProperty("shiroEnvironmentClass");
+ if(_Strings.isNotEmpty(customShiroEnvironmentClassName)) {
+ ctx.setInitParameter("shiroEnvironmentClass", customShiroEnvironmentClassName);
+ }
+
+ val listener = createListener(EnvironmentLoaderListenerForIsis.class);
+ return Can.ofSingleton(listener);
+
+ }
+
+
+
+}
diff --cc valuetypes/sse/ui/src/main/java/org/apache/isis/extensions/sse/ui/webmodule/ServerSentEventsServlet.java
index 7a706e3,0000000..a4484a0
mode 100644,000000..100644
--- a/valuetypes/sse/ui/src/main/java/org/apache/isis/extensions/sse/ui/webmodule/ServerSentEventsServlet.java
+++ b/valuetypes/sse/ui/src/main/java/org/apache/isis/extensions/sse/ui/webmodule/ServerSentEventsServlet.java
@@@ -1,165 -1,0 +1,165 @@@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.isis.extensions.sse.ui.webmodule;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
+
- import javax.inject.Inject;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
++import org.springframework.beans.factory.annotation.Autowired;
++
+import org.apache.isis.applib.util.JaxbAdapters;
+import org.apache.isis.core.commons.internal.base._Strings;
+import org.apache.isis.core.commons.internal.context._Context;
+import org.apache.isis.extensions.sse.applib.service.SseChannel;
+import org.apache.isis.extensions.sse.applib.service.SseService;
+import org.apache.isis.extensions.sse.applib.value.ListeningMarkup;
+
+import static org.apache.isis.core.commons.internal.base._With.requires;
+
+import lombok.val;
+
+/**
+ * Server-sent events.
+ *
+ * @see <a href="https://www.w3schools.com/html/html5_serversentevents.asp">Server Sent Events (@ w3schools)</a>
+ *
+ * @since 2.0
+ *
+ */
+//@WebServlet(value="/sse", asyncSupported=true)
+public class ServerSentEventsServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+ private ExecutorService threadPool;
+
- @Inject
- private SseService sseService;
++ @Autowired private SseService sseService;
+
+ @Override
+ public void init() throws ServletException {
+ super.init();
+ threadPool = ForkJoinPool.commonPool();
+
+ requires(sseService, "sseService");
+ }
+
+ @Override
+ public void destroy() {
+ threadPool.shutdown();
+ super.destroy();
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+
+ val eventStreamType = parseEventStreamType(request);
+ val eventStream = eventStreamType.flatMap(sseService::lookupByType)
+ .orElse(null);
+
+ if(eventStream==null) {
+ response.setStatus(200);
+ response.setContentType(null);
+ response.flushBuffer();
+ return;
+ }
+
+ response.setStatus(200);
+ response.setContentType("text/event-stream");
+ response.setCharacterEncoding("UTF-8");
+ //response.setHeader("Access-Control-Allow-Origin", "*"); // not secure, can be used for debugging
+ response.setHeader("Cache-Control", "no-cache,no-store");
+ response.flushBuffer();
+
+ final AsyncContext asyncContext = request.startAsync();
+
+ //XXX javac explicitly requires curly-braces here (to tell it we want a Runnable)
+ threadPool.submit(()->{fork(asyncContext, eventStream);});
+
+ }
+
+ // -- HELPER
+
+ private void fork(final AsyncContext asyncContext, final SseChannel eventStream) {
+
+ val response = asyncContext.getResponse();
+ val marshaller = new JaxbAdapters.MarkupAdapter();
+
+ eventStream.listenWhile(source->{
+
+ if(threadPool.isShutdown()) {
+ return false; // stop listening
+ }
+
+ try {
+
+ val writer = response.getWriter(); // don't close the writer, its likely to be reused
+ if(writer==null) {
+ return false; // stop listening
+ }
+
+ val payload = marshaller.marshal(ListeningMarkup.valueOfHtml(source.getPayload()));
+
+ writer
+ .append("data: ")
+ .append(payload)
+ .append("\n\n")
+ .flush();
+
+ return true; // continue listening
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false; // stop listening
+ }
+
+ });
+
+ // now we wait until the eventStream closes
+ try {
+ eventStream.awaitClose();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // Completes the asynchronous operation that was started on the request
+ // that was used to initialize this AsyncContext.
+ asyncContext.complete();
+
+ }
+
+ private Optional<Class<?>> parseEventStreamType(HttpServletRequest request) {
+ val eventStreamId = request.getParameter("eventStream");
+ if(_Strings.isNullOrEmpty(eventStreamId)) {
+ return Optional.empty();
+ }
+ try {
+ return Optional.of(_Context.loadClass(eventStreamId));
+ } catch (Throwable e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+}
diff --cc viewers/restfulobjects/jaxrs-resteasy-4/src/main/java/org/apache/isis/viewer/restfulobjects/jaxrsresteasy4/webmodule/WebModuleJaxrsResteasy4.java
index 63913b1,0000000..04ee46e
mode 100644,000000..100644
--- a/viewers/restfulobjects/jaxrs-resteasy-4/src/main/java/org/apache/isis/viewer/restfulobjects/jaxrsresteasy4/webmodule/WebModuleJaxrsResteasy4.java
+++ b/viewers/restfulobjects/jaxrs-resteasy-4/src/main/java/org/apache/isis/viewer/restfulobjects/jaxrsresteasy4/webmodule/WebModuleJaxrsResteasy4.java
@@@ -1,128 -1,0 +1,142 @@@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.isis.viewer.restfulobjects.jaxrsresteasy4.webmodule;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletException;
+
++import org.jboss.resteasy.core.providerfactory.ResteasyProviderFactoryImpl;
++import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.applib.services.inject.ServiceInjector;
+import org.apache.isis.core.commons.collections.Can;
+import org.apache.isis.core.config.RestEasyConfiguration;
+import org.apache.isis.viewer.restfulobjects.viewer.webmodule.IsisRestfulObjectsSessionFilter;
+import org.apache.isis.viewer.restfulobjects.viewer.webmodule.IsisTransactionFilterForRestfulObjects;
+import org.apache.isis.viewer.restfulobjects.viewer.webmodule.auth.AuthenticationSessionStrategyBasicAuth;
+import org.apache.isis.webapp.modules.WebModuleAbstract;
+import org.apache.isis.webapp.modules.WebModuleContext;
+
+import lombok.Getter;
+
+/**
+ * WebModule that provides the RestfulObjects Viewer.
+ *
+ * @since 2.0
++ *
++ * @implNote CDI feels responsible to resolve injection points for any Servlet or Filter
++ * we register programmatically on the ServletContext.
++ * As long as injection points are considered to be resolved by Spring, we can workaround this fact:
++ * By replacing annotations {@code @Inject} with {@code @Autowire} for any Servlet or Filter,
++ * that get contributed by a WebModule, these will be ignored by CDI.
++ *
+ */
+@Service
+@Named("isisRoViewer.WebModuleJaxrsRestEasy4")
+@Order(OrderPrecedence.MIDPOINT - 80)
+@Qualifier("JaxrsRestEasy4")
+public final class WebModuleJaxrsResteasy4 extends WebModuleAbstract {
+
+ private static final String ISIS_SESSION_FILTER_FOR_RESTFUL_OBJECTS = "IsisSessionFilterForRestfulObjects";
+ private static final String ISIS_TRANSACTION_FILTER = "IsisTransactionFilterForRestfulObjects";
+
+ private final RestEasyConfiguration restEasyConfiguration;
+
+ private final String restfulPath;
+ private final String urlPattern;
+
+ @Inject
+ public WebModuleJaxrsResteasy4(
+ final RestEasyConfiguration restEasyConfiguration,
+ final ServiceInjector serviceInjector) {
+ super(serviceInjector);
+ this.restEasyConfiguration = restEasyConfiguration;
+ this.restfulPath = this.restEasyConfiguration.getJaxrs().getDefaultPath() + "/";
+ this.urlPattern = this.restfulPath + "*";
+ }
+
+ @Getter
+ private final String name = "JaxrsRestEasy4";
+
+ @Override
+ public void prepare(WebModuleContext ctx) {
++
++ // forces RuntimeDelegate.getInstance() to be provided by RestEasy
++ // (and not by eg. the JEE container if any)
++ ResteasyProviderFactory.setInstance(new ResteasyProviderFactoryImpl());
++
+ super.prepare(ctx);
+
+ if(!isApplicable(ctx)) {
+ return;
+ }
+
+ ctx.addViewer("restfulobjects");
+ ctx.addProtectedPath(urlPattern);
+ }
+
+ @Override
+ public Can<ServletContextListener> init(ServletContext ctx) throws ServletException {
+
+ registerFilter(ctx, ISIS_SESSION_FILTER_FOR_RESTFUL_OBJECTS, IsisRestfulObjectsSessionFilter.class)
+ .ifPresent(filterReg -> {
+ // this is mapped to the entire application;
+ // however the IsisSessionFilter will
+ // "notice" if the session filter has already been
+ // executed for the request pipeline, and if so will do nothing
+ filterReg.addMappingForUrlPatterns(
+ null,
+ true,
+ this.urlPattern);
+
+ filterReg.setInitParameter(
+ "authenticationSessionStrategy",
+ AuthenticationSessionStrategyBasicAuth.class.getName());
+ filterReg.setInitParameter(
+ "whenNoSession", // what to do if no session was found ...
+ "auto"); // ... 401 and a basic authentication challenge if request originates from web browser
+ filterReg.setInitParameter(
+ "passThru",
+ String.join(",",
+ this.restfulPath + "swagger",
+ this.restfulPath + "health"));
+
+ } );
+
+ registerFilter(ctx, ISIS_TRANSACTION_FILTER, IsisTransactionFilterForRestfulObjects.class)
+ .ifPresent(filterReg -> {
+ filterReg.addMappingForUrlPatterns(
+ null,
+ true,
+ this.urlPattern);
+ });
+
+
+ return Can.empty(); // registers no listeners
+ }
+
+
+}
diff --cc viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/webmodule/IsisRestfulObjectsSessionFilter.java
index 5fdf4bf,0000000..d0575e4
mode 100644,000000..100644
--- a/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/webmodule/IsisRestfulObjectsSessionFilter.java
+++ b/viewers/restfulobjects/viewer/src/main/java/org/apache/isis/viewer/restfulobjects/viewer/webmodule/IsisRestfulObjectsSessionFilter.java
@@@ -1,435 -1,0 +1,435 @@@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.isis.viewer.restfulobjects.viewer.webmodule;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
++import org.springframework.beans.factory.annotation.Autowired;
++
+import org.apache.isis.core.commons.internal.base._Strings;
+import org.apache.isis.core.commons.internal.collections._Lists;
+import org.apache.isis.core.commons.internal.factory.InstanceUtil;
+import org.apache.isis.core.metamodel.commons.StringExtensions;
+import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
+import org.apache.isis.core.metamodel.specloader.validator.MetaModelInvalidException;
+import org.apache.isis.runtime.session.IsisSessionFactory;
+import org.apache.isis.viewer.restfulobjects.viewer.webmodule.auth.AuthenticationSessionStrategy;
+import org.apache.isis.viewer.restfulobjects.viewer.webmodule.auth.AuthenticationSessionStrategyDefault;
+import org.apache.isis.webapp.modules.templresources.TemplateResourceCachingFilter;
+
+import static org.apache.isis.core.commons.internal.base._With.requires;
+
+import lombok.val;
+
+/**
+ * Filter for RestfulObjects.
+ *
+ * authenticate user, set up an Isis session
+ */
+//@WebFilter(
+// servletNames={"RestfulObjectsRestEasyDispatcher"}, // this is mapped to the entire application;
+// // however the IsisSessionFilter will
+// // "notice" if the session filter has already been
+// // executed for the request pipeline, and if so will do nothing
+// initParams={
+// @WebInitParam(
+// name="authenticationSessionStrategy",
+// value="org.apache.isis.viewer.restfulobjects.server.authentication.AuthenticationSessionStrategyBasicAuth"), // authentication required for REST
+// @WebInitParam(
+// name="whenNoSession", // what to do if no session was found ...
+// value="auto"), // ... 401 and a basic authentication challenge if request originates from web browser
+// @WebInitParam(name="passThru", value="/restful/swagger,/restful/health") //TODO[ISIS-1895] the restful path is configured elsewhere
+//})
+public class IsisRestfulObjectsSessionFilter implements Filter {
+
+ /**
+ * Recommended standard init parameter key for filters and servlets to
+ * lookup an implementation of {@link AuthenticationSessionStrategy}.
+ */
+ public static final String AUTHENTICATION_SESSION_STRATEGY_KEY = "authenticationSessionStrategy";
+
+ /**
+ * Default value for {@link #AUTHENTICATION_SESSION_STRATEGY_KEY} if not specified.
+ */
+ public static final String AUTHENTICATION_SESSION_STRATEGY_DEFAULT = AuthenticationSessionStrategyDefault.class.getName();
+
+ /**
+ * Init parameter key for backward compatibility; if logonPage set then
+ * assume 'restricted' handling.
+ */
+ public static final String LOGON_PAGE_KEY = "logonPage";
+
+
+ /**
+ * Init parameter key for what should be done if no session was found.
+ *
+ * <p>
+ * Valid values are:
+ * <ul>
+ * <li>unauthorized - issue a 401 response.
+ * <li>basicAuthChallenge - issue a basic auth 401 challenge. The idea here
+ * is that the configured logon strategy should handle the next request
+ * <li>restricted - allow access but only to a restricted (comma-separated)
+ * list of paths. Access elsewhere should be redirected to the first of
+ * these paths
+ * <li>continue - allow the request to continue (eg if there is no security
+ * requirements)
+ * </ul>
+ */
+ public static final String WHEN_NO_SESSION_KEY = "whenNoSession";
+
+ /**
+ * Which URLs to ignore (eg <code>/restful/swagger</code> so that swagger specs can be accessed from the swagger-ui)
+ */
+ public static final String PASS_THRU_KEY = "passThru";
+
+ /**
+ * Init parameter key to read the restricted list of paths (if
+ * {@link #WHEN_NO_SESSION_KEY} is for {@link WhenNoSession#RESTRICTED}).
+ *
+ * <p>
+ * The servlets mapped to these paths are expected to be able to deal with
+ * there being no session. Typically they will be logon pages.
+ */
+ public static final String RESTRICTED_KEY = "restricted";
+
+ /**
+ * Init parameter key to redirect to if an exception occurs.
+ */
+ public static final String REDIRECT_TO_ON_EXCEPTION_KEY = "redirectToOnException";
+
+ /**
+ * Init parameter key for which extensions should be ignored (typically,
+ * mappings for other viewers within the webapp context).
+ *
+ * <p>
+ * It can also be used to specify ignored static resources (though putting
+ * the {@link TemplateResourceCachingFilter} first in the <tt>web.xml</tt>
+ * accomplishes the same thing).
+ *
+ * <p>
+ * The value is expected as a comma separated list.
+ */
+ public static final String IGNORE_EXTENSIONS_KEY = "ignoreExtensions";
+ /**
+ * Somewhat hacky, add this to the query
+ */
+ public static final String ISIS_SESSION_FILTER_QUERY_STRING_FORCE_LOGOUT = "__isis_force_logout";
+
+ private static final Function<String, Pattern> STRING_TO_PATTERN = (final String input) -> {
+ return Pattern.compile(".*\\." + input);
+ };
+
- @Inject
- private IsisSessionFactory isisSessionFactory;
- @Inject
- private SpecificationLoader specificationLoader;
++ @Autowired private IsisSessionFactory isisSessionFactory;
++ @Autowired private SpecificationLoader specificationLoader;
+
+ private List<String> passThruList = Collections.emptyList();
+
+ static void redirect(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final String redirectTo) throws IOException {
+ httpResponse.sendRedirect(StringExtensions.combinePath(httpRequest.getContextPath(), redirectTo));
+ }
+
+ public enum WhenNoSession {
+ UNAUTHORIZED("unauthorized") {
+ @Override
+ public void handle(final IsisRestfulObjectsSessionFilter filter, final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final FilterChain chain) throws IOException, ServletException {
+ httpResponse.sendError(401);
+ }
+ },
+ BASIC_AUTH_CHALLENGE("basicAuthChallenge") {
+ @Override
+ public void handle(final IsisRestfulObjectsSessionFilter filter, final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final FilterChain chain) throws IOException, ServletException {
+ httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"Apache Isis\"");
+ httpResponse.sendError(401);
+ }
+ },
+ AUTO("auto") {
+ @Override
+ public void handle(final IsisRestfulObjectsSessionFilter filter, final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final FilterChain chain) throws IOException, ServletException {
+ if(fromWebBrowser(httpRequest)) {
+ httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"Apache Isis\"");
+ }
+ httpResponse.sendError(401);
+ }
+
+ private boolean fromWebBrowser(final HttpServletRequest httpRequest) {
+ String accept = httpRequest.getHeader("Accept");
+ return accept.contains("text/html");
+ }
+ },
+ /**
+ * the destination servlet is expected to know that there will be no open session, and handle the case appropriately
+ */
+ CONTINUE("continue") {
+ @Override
+ public void handle(final IsisRestfulObjectsSessionFilter filter, final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final FilterChain chain) throws IOException, ServletException {
+ chain.doFilter(httpRequest, httpResponse);
+ }
+ },
+ /**
+ * Allow access to a restricted list of URLs (else redirect to the first of that list of URLs)
+ */
+ RESTRICTED("restricted") {
+ @Override
+ public void handle(final IsisRestfulObjectsSessionFilter filter, final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, final FilterChain chain) throws IOException, ServletException {
+
+ if (filter.restrictedPaths.contains(httpRequest.getServletPath())) {
+ chain.doFilter(httpRequest, httpResponse);
+ return;
+ }
+ redirect(httpRequest, httpResponse, filter.restrictedPaths.get(0));
+ }
+
+ };
+ private final String initParamValue;
+
+ private WhenNoSession(final String initParamValue) {
+ this.initParamValue = initParamValue;
+ }
+
+ public static WhenNoSession lookup(final String whenNoSessionStr) {
+ for (final WhenNoSession wns : values()) {
+ if (wns.initParamValue.equals(whenNoSessionStr)) {
+ return wns;
+ }
+ }
+ throw new IllegalStateException("require an init-param of '" + WHEN_NO_SESSION_KEY + "', taking a value of " + WhenNoSession.values());
+ }
+
+ public abstract void handle(IsisRestfulObjectsSessionFilter filter, HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain chain) throws IOException, ServletException;
+ }
+
+
+ private AuthenticationSessionStrategy authSessionStrategy;
+ private List<String> restrictedPaths;
+ private WhenNoSession whenNotAuthenticated;
+ private String redirectToOnException;
+ private Collection<Pattern> ignoreExtensions;
+
+
+
+
+ // /////////////////////////////////////////////////////////////////
+ // init, destroy
+ // /////////////////////////////////////////////////////////////////
+
+ @Override
+ public void init(final FilterConfig config) throws ServletException {
+ authSessionStrategy = lookup(config.getInitParameter(AUTHENTICATION_SESSION_STRATEGY_KEY));
+ lookupWhenNoSession(config);
+ lookupPassThru(config);
+ lookupRedirectToOnException(config);
+ lookupIgnoreExtensions(config);
+ }
+
+ /**
+ * Public visibility so can also be used by servlets.
+ */
+ public static AuthenticationSessionStrategy lookup(String authLookupStrategyClassName) {
+ if (authLookupStrategyClassName == null) {
+ authLookupStrategyClassName = AUTHENTICATION_SESSION_STRATEGY_DEFAULT;
+ }
+ return (AuthenticationSessionStrategy) InstanceUtil.createInstance(authLookupStrategyClassName);
+ }
+
+ private void lookupWhenNoSession(final FilterConfig config) {
+
+ final String whenNoSessionStr = config.getInitParameter(WHEN_NO_SESSION_KEY);
+
+ // backward compatibility
+ final String logonPage = config.getInitParameter(LOGON_PAGE_KEY);
+ if (logonPage != null) {
+ if (whenNoSessionStr != null) {
+ throw new IllegalStateException(String.format(
+ "The init-param '%s' is only provided for backwards compatibility; "
+ + "remove if the init-param '%s' has been specified", LOGON_PAGE_KEY, WHEN_NO_SESSION_KEY));
+ } else {
+ // default whenNotAuthenticated and allow access through to the logonPage
+ whenNotAuthenticated = WhenNoSession.RESTRICTED;
+ this.restrictedPaths = _Lists.of(logonPage);
+ return;
+ }
+ }
+
+ whenNotAuthenticated = WhenNoSession.lookup(whenNoSessionStr);
+ if (whenNotAuthenticated == WhenNoSession.RESTRICTED) {
+ final String restrictedPathsStr = config.getInitParameter(RESTRICTED_KEY);
+ if (restrictedPathsStr == null) {
+ throw new IllegalStateException(String.format("Require an init-param of '%s' key to be set.", RESTRICTED_KEY));
+ }
+ this.restrictedPaths =
+ _Strings.splitThenStream(restrictedPathsStr, ",")
+ .collect(Collectors.toList());
+
+ }
+
+ }
+
+ void lookupPassThru(final FilterConfig config) {
+ this.passThruList = lookupAndParsePassThru(config);
+ }
+
+ List<String> lookupAndParsePassThru(final FilterConfig config) {
+ final String passThru = config.getInitParameter(PASS_THRU_KEY);
+ return passThru != null && !passThru.equals("")
+ ? Arrays.asList(passThru.split(","))
+ : Collections.<String>emptyList();
+ }
+
+ private void lookupRedirectToOnException(final FilterConfig config) {
+ redirectToOnException = config.getInitParameter(REDIRECT_TO_ON_EXCEPTION_KEY);
+ }
+
+ private void lookupIgnoreExtensions(final FilterConfig config) {
+ ignoreExtensions = Collections.unmodifiableCollection(parseIgnorePatterns(config)
+ .collect(Collectors.toList()));
+ }
+
+ private Stream<Pattern> parseIgnorePatterns(final FilterConfig config) {
+ final String ignoreExtensionsStr = config.getInitParameter(IGNORE_EXTENSIONS_KEY);
+ if (ignoreExtensionsStr != null) {
+ final Stream<String> ignoreExtensions = _Strings.splitThenStream(ignoreExtensionsStr, ",");
+ return ignoreExtensions.map(STRING_TO_PATTERN);
+ }
+ return Stream.empty();
+ }
+
+
+ @Override
+ public void destroy() {
+ }
+
+ // /////////////////////////////////////////////////////////////////
+ // doFilter
+ // /////////////////////////////////////////////////////////////////
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
+
+ requires(isisSessionFactory, "isisSessionFactory");
+ requires(specificationLoader, "specificationLoader");
+
+ ensureMetamodelIsValid(specificationLoader);
+
+ val httpServletRequest = (HttpServletRequest) request;
+ val httpServletResponse = (HttpServletResponse) response;
+
+ try {
+ val queryString = httpServletRequest.getQueryString();
+ if (queryString != null && queryString
+ .contains(ISIS_SESSION_FILTER_QUERY_STRING_FORCE_LOGOUT)) {
+
+ authSessionStrategy.invalidate(httpServletRequest, httpServletResponse);
+ return;
+ }
+
+ if (requestIsIgnoreExtension(this, httpServletRequest) ||
+ TemplateResourceCachingFilter.isCachedResource(httpServletRequest)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ if(requestIsPassThru(httpServletRequest)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ // authenticate
+ val authenticationSession =
+ authSessionStrategy.lookupValid(httpServletRequest, httpServletResponse);
+ if (authenticationSession != null) {
+
+ authSessionStrategy.bind(httpServletRequest, httpServletResponse, authenticationSession);
+
+ isisSessionFactory.openSession(authenticationSession);
+ chain.doFilter(request, response);
+
+ return;
+ }
+
+ try {
+ whenNotAuthenticated.handle(this, httpServletRequest, httpServletResponse, chain);
+ } catch (final RuntimeException | IOException | ServletException ex) {
+ // in case the destination servlet cannot cope, but we've
+ // been told to redirect elsewhere
+ if (redirectToOnException != null) {
+ redirect(httpServletRequest, httpServletResponse, redirectToOnException);
+ return;
+ }
+ throw ex;
+ }
+
+ } finally {
+ isisSessionFactory.closeSession();
+ }
+
+ }
+
+
+ private static void ensureMetamodelIsValid(SpecificationLoader specificationLoader) {
+ val validationResult = specificationLoader.getValidationResult();
+ if(validationResult.hasFailures()) {
+ throw new MetaModelInvalidException(validationResult.getAsLineNumberedString());
+ }
+ }
+
+
+ protected boolean requestIsPassThru(final HttpServletRequest httpServletRequest) {
+ final String requestURI = httpServletRequest.getRequestURI();
+ for (final String passThru : passThruList) {
+ if(requestURI.startsWith(passThru)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean requestIsIgnoreExtension(
+ final IsisRestfulObjectsSessionFilter filter,
+ final HttpServletRequest httpRequest) {
+
+ val servletPath = httpRequest.getServletPath();
+ for (final Pattern extension : filter.ignoreExtensions) {
+ if (extension.matcher(servletPath).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}