Reintroduce web socket support.
authorMathieu Baudier <mbaudier@argeo.org>
Wed, 25 May 2022 07:29:00 +0000 (09:29 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Wed, 25 May 2022 07:29:00 +0000 (09:29 +0200)
25 files changed:
Makefile
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsExceptionsChain.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLoginServlet.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLogoutServlet.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsPrivateServletContext.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsSessionDescriptor.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsTokenServlet.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/TokenDescriptor.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/package-info.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyServiceFactory.java
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketView.java [new file with mode: 0644]
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/package-info.java [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/.classpath [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/.gitignore [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/.project [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/META-INF/.gitignore [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/bnd.bnd [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/build.properties [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java [new file with mode: 0644]
eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/package-info.java [new file with mode: 0644]
jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrReadServlet.java [new file with mode: 0644]
jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrWriteServlet.java [new file with mode: 0644]

index d708230392ea30e897bee4b30f1d6c85b0a83132..af16906ccffc62dafadb8b94c353b9d423eb83ae 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,7 @@ org.argeo.api.cms \
 org.argeo.cms \
 org.argeo.cms.pgsql \
 org.argeo.cms.ux \
+eclipse/org.argeo.ext.equinox.jetty \
 eclipse/org.argeo.cms.servlet \
 eclipse/org.argeo.cms.swt \
 eclipse/org.argeo.cms.e4 \
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsExceptionsChain.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsExceptionsChain.java
new file mode 100644 (file)
index 0000000..fb289c1
--- /dev/null
@@ -0,0 +1,154 @@
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.cms.CmsLog;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Serialisable wrapper of a {@link Throwable}. */
+public class CmsExceptionsChain {
+       public final static CmsLog log = CmsLog.getLog(CmsExceptionsChain.class);
+
+       private List<SystemException> exceptions = new ArrayList<>();
+
+       public CmsExceptionsChain() {
+               super();
+       }
+
+       public CmsExceptionsChain(Throwable exception) {
+               writeException(exception);
+               if (log.isDebugEnabled())
+                       log.error("Exception chain", exception);
+       }
+
+       public String toJsonString(ObjectMapper objectMapper) {
+               try {
+                       return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(this);
+               } catch (JsonProcessingException e) {
+                       throw new IllegalStateException("Cannot write system exceptions " + toString(), e);
+               }
+       }
+
+       public void writeAsJson(ObjectMapper objectMapper, Writer writer) {
+               try {
+                       JsonGenerator jg = objectMapper.writerWithDefaultPrettyPrinter().getFactory().createGenerator(writer);
+                       jg.writeObject(this);
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot write system exceptions " + toString(), e);
+               }
+       }
+
+       public void writeAsJson(ObjectMapper objectMapper, HttpServletResponse resp) {
+               try {
+                       resp.setContentType("application/json");
+                       resp.setStatus(500);
+                       writeAsJson(objectMapper, resp.getWriter());
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot write system exceptions " + toString(), e);
+               }
+       }
+
+       /** recursive */
+       protected void writeException(Throwable exception) {
+               SystemException systemException = new SystemException(exception);
+               exceptions.add(systemException);
+               Throwable cause = exception.getCause();
+               if (cause != null)
+                       writeException(cause);
+       }
+
+       public List<SystemException> getExceptions() {
+               return exceptions;
+       }
+
+       public void setExceptions(List<SystemException> exceptions) {
+               this.exceptions = exceptions;
+       }
+
+       /** An exception in the chain. */
+       public static class SystemException {
+               private String type;
+               private String message;
+               private List<String> stackTrace;
+
+               public SystemException() {
+               }
+
+               public SystemException(Throwable exception) {
+                       this.type = exception.getClass().getName();
+                       this.message = exception.getMessage();
+                       this.stackTrace = new ArrayList<>();
+                       StackTraceElement[] elems = exception.getStackTrace();
+                       for (int i = 0; i < elems.length; i++)
+                               stackTrace.add("at " + elems[i].toString());
+               }
+
+               public String getType() {
+                       return type;
+               }
+
+               public void setType(String type) {
+                       this.type = type;
+               }
+
+               public String getMessage() {
+                       return message;
+               }
+
+               public void setMessage(String message) {
+                       this.message = message;
+               }
+
+               public List<String> getStackTrace() {
+                       return stackTrace;
+               }
+
+               public void setStackTrace(List<String> stackTrace) {
+                       this.stackTrace = stackTrace;
+               }
+
+               @Override
+               public String toString() {
+                       return "System exception: " + type + ", " + message + ", " + stackTrace;
+               }
+
+       }
+
+       @Override
+       public String toString() {
+               return exceptions.toString();
+       }
+
+//     public static void main(String[] args) throws Exception {
+//             try {
+//                     try {
+//                             try {
+//                                     testDeeper();
+//                             } catch (Exception e) {
+//                                     throw new Exception("Less deep exception", e);
+//                             }
+//                     } catch (Exception e) {
+//                             throw new RuntimeException("Top exception", e);
+//                     }
+//             } catch (Exception e) {
+//                     CmsExceptionsChain vjeSystemErrors = new CmsExceptionsChain(e);
+//                     ObjectMapper objectMapper = new ObjectMapper();
+//                     System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(vjeSystemErrors));
+//                     System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(e));
+//                     e.printStackTrace();
+//             }
+//     }
+//
+//     static void testDeeper() throws Exception {
+//             throw new IllegalStateException("Deep exception");
+//     }
+
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLoginServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLoginServlet.java
new file mode 100644 (file)
index 0000000..29a3137
--- /dev/null
@@ -0,0 +1,112 @@
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.CmsSessionId;
+import org.argeo.cms.auth.RemoteAuthCallback;
+import org.argeo.cms.auth.RemoteAuthCallbackHandler;
+import org.argeo.cms.servlet.ServletHttpRequest;
+import org.argeo.cms.servlet.ServletHttpResponse;
+import org.osgi.service.useradmin.Authorization;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Externally authenticate an http session. */
+public class CmsLoginServlet extends HttpServlet {
+       public final static String PARAM_USERNAME = "username";
+       public final static String PARAM_PASSWORD = "password";
+
+       private static final long serialVersionUID = 2478080654328751539L;
+       private ObjectMapper objectMapper = new ObjectMapper();
+
+       @Override
+       protected void doGet(HttpServletRequest request, HttpServletResponse response)
+                       throws ServletException, IOException {
+               doPost(request, response);
+       }
+
+       @Override
+       protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               LoginContext lc = null;
+               String username = req.getParameter(PARAM_USERNAME);
+               String password = req.getParameter(PARAM_PASSWORD);
+               ServletHttpRequest request = new ServletHttpRequest(req);
+               ServletHttpResponse response = new ServletHttpResponse(resp);
+               try {
+                       lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response) {
+                               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                                       for (Callback callback : callbacks) {
+                                               if (callback instanceof NameCallback && username != null)
+                                                       ((NameCallback) callback).setName(username);
+                                               else if (callback instanceof PasswordCallback && password != null)
+                                                       ((PasswordCallback) callback).setPassword(password.toCharArray());
+                                               else if (callback instanceof RemoteAuthCallback) {
+                                                       ((RemoteAuthCallback) callback).setRequest(request);
+                                                       ((RemoteAuthCallback) callback).setResponse(response);
+                                               }
+                                       }
+                               }
+                       });
+                       lc.login();
+
+                       Subject subject = lc.getSubject();
+                       CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class));
+                       if (cmsSessionId == null) {
+                               resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+                               return;
+                       }
+                       Authorization authorization = extractFrom(subject.getPrivateCredentials(Authorization.class));
+                       Locale locale = extractFrom(subject.getPublicCredentials(Locale.class));
+
+                       CmsSessionDescriptor cmsSessionDescriptor = new CmsSessionDescriptor(authorization.getName(),
+                                       cmsSessionId.getUuid().toString(), authorization.getRoles(), authorization.toString(),
+                                       locale != null ? locale.toString() : null);
+
+                       resp.setContentType("application/json");
+                       JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter());
+                       jg.writeObject(cmsSessionDescriptor);
+
+                       String redirectTo = redirectTo(req);
+                       if (redirectTo != null)
+                               resp.sendRedirect(redirectTo);
+               } catch (LoginException e) {
+                       resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+                       return;
+               }
+       }
+
+       protected <T> T extractFrom(Set<T> creds) {
+               if (creds.size() > 0)
+                       return creds.iterator().next();
+               else
+                       return null;
+       }
+
+       /**
+        * To be overridden in order to return a richer {@link CmsSessionDescriptor} to
+        * be serialized.
+        */
+       protected CmsSessionDescriptor enrichJson(CmsSessionDescriptor cmsSessionDescriptor) {
+               return cmsSessionDescriptor;
+       }
+
+       protected String redirectTo(HttpServletRequest request) {
+               return null;
+       }
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLogoutServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsLogoutServlet.java
new file mode 100644 (file)
index 0000000..0628eae
--- /dev/null
@@ -0,0 +1,79 @@
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.CmsSessionId;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.auth.RemoteAuthCallback;
+import org.argeo.cms.auth.RemoteAuthCallbackHandler;
+import org.argeo.cms.servlet.ServletHttpRequest;
+import org.argeo.cms.servlet.ServletHttpResponse;
+
+/** Externally authenticate an http session. */
+public class CmsLogoutServlet extends HttpServlet {
+       private static final long serialVersionUID = 2478080654328751539L;
+
+       @Override
+       protected void doGet(HttpServletRequest request, HttpServletResponse response)
+                       throws ServletException, IOException {
+               doPost(request, response);
+       }
+
+       @Override
+       protected void doPost(HttpServletRequest request, HttpServletResponse response)
+                       throws ServletException, IOException {
+               ServletHttpRequest httpRequest = new ServletHttpRequest(request);
+               ServletHttpResponse httpResponse = new ServletHttpResponse(response);
+               LoginContext lc = null;
+               try {
+                       lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER,
+                                       new RemoteAuthCallbackHandler(httpRequest, httpResponse) {
+                                               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                                                       for (Callback callback : callbacks) {
+                                                               if (callback instanceof RemoteAuthCallback) {
+                                                                       ((RemoteAuthCallback) callback).setRequest(httpRequest);
+                                                                       ((RemoteAuthCallback) callback).setResponse(httpResponse);
+                                                               }
+                                                       }
+                                               }
+                                       });
+                       lc.login();
+
+                       Subject subject = lc.getSubject();
+                       CmsSessionId cmsSessionId = extractFrom(subject.getPrivateCredentials(CmsSessionId.class));
+                       if (cmsSessionId != null) {// logged in
+                               CurrentUser.logoutCmsSession(subject);
+                       }
+
+               } catch (LoginException e) {
+                       // ignore
+               }
+
+               String redirectTo = redirectTo(request);
+               if (redirectTo != null)
+                       response.sendRedirect(redirectTo);
+       }
+
+       protected <T> T extractFrom(Set<T> creds) {
+               if (creds.size() > 0)
+                       return creds.iterator().next();
+               else
+                       return null;
+       }
+
+       protected String redirectTo(HttpServletRequest request) {
+               return null;
+       }
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsPrivateServletContext.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsPrivateServletContext.java
new file mode 100644 (file)
index 0000000..cec04d2
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.security.AccessControlContext;
+import java.security.PrivilegedAction;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.cms.auth.RemoteAuthCallbackHandler;
+import org.argeo.cms.auth.RemoteAuthUtils;
+import org.argeo.cms.servlet.ServletHttpRequest;
+import org.argeo.cms.servlet.ServletHttpResponse;
+import org.osgi.service.http.context.ServletContextHelper;
+
+/** Manages security access to servlets. */
+public class CmsPrivateServletContext extends ServletContextHelper {
+       public final static String LOGIN_PAGE = "argeo.cms.integration.loginPage";
+       public final static String LOGIN_SERVLET = "argeo.cms.integration.loginServlet";
+       private String loginPage;
+       private String loginServlet;
+
+       public void init(Map<String, String> properties) {
+               loginPage = properties.get(LOGIN_PAGE);
+               loginServlet = properties.get(LOGIN_SERVLET);
+       }
+
+       /**
+        * Add the {@link AccessControlContext} as a request attribute, or redirect to
+        * the login page.
+        */
+       @Override
+       public boolean handleSecurity(final HttpServletRequest req, HttpServletResponse resp) throws IOException {
+               LoginContext lc = null;
+               ServletHttpRequest request = new ServletHttpRequest(req);
+               ServletHttpResponse response = new ServletHttpResponse(resp);
+
+               String pathInfo = req.getPathInfo();
+               String servletPath = req.getServletPath();
+               if ((pathInfo != null && (servletPath + pathInfo).equals(loginPage)) || servletPath.contentEquals(loginServlet))
+                       return true;
+               try {
+                       lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response));
+                       lc.login();
+               } catch (LoginException e) {
+                       lc = processUnauthorized(req, resp);
+                       if (lc == null)
+                               return false;
+               }
+               Subject.doAs(lc.getSubject(), new PrivilegedAction<Void>() {
+
+                       @Override
+                       public Void run() {
+                               // TODO also set login context in order to log out ?
+                               RemoteAuthUtils.configureRequestSecurity(request);
+                               return null;
+                       }
+
+               });
+
+               return true;
+       }
+
+       @Override
+       public void finishSecurity(HttpServletRequest req, HttpServletResponse resp) {
+               RemoteAuthUtils.clearRequestSecurity(new ServletHttpRequest(req));
+       }
+
+       protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) {
+               try {
+                       response.sendRedirect(loginPage);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot redirect to login page", e);
+               }
+               return null;
+       }
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsSessionDescriptor.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsSessionDescriptor.java
new file mode 100644 (file)
index 0000000..30de616
--- /dev/null
@@ -0,0 +1,96 @@
+package org.argeo.cms.integration;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.argeo.api.cms.CmsSession;
+import org.osgi.service.useradmin.Authorization;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/** A serializable descriptor of an internal {@link CmsSession}. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CmsSessionDescriptor implements Serializable, Authorization {
+       private static final long serialVersionUID = 8592162323372641462L;
+
+       private String name;
+       private String cmsSessionId;
+       private String displayName;
+       private String locale;
+       private Set<String> roles;
+
+       public CmsSessionDescriptor() {
+       }
+
+       public CmsSessionDescriptor(String name, String cmsSessionId, String[] roles, String displayName, String locale) {
+               this.name = name;
+               this.displayName = displayName;
+               this.cmsSessionId = cmsSessionId;
+               this.locale = locale;
+               this.roles = Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles)));
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public void setName(String name) {
+               this.name = name;
+       }
+
+       public String getDisplayName() {
+               return displayName;
+       }
+
+       public void setDisplayName(String displayName) {
+               this.displayName = displayName;
+       }
+
+       public String getCmsSessionId() {
+               return cmsSessionId;
+       }
+
+       public void setCmsSessionId(String cmsSessionId) {
+               this.cmsSessionId = cmsSessionId;
+       }
+
+       public Boolean isAnonymous() {
+               return name == null;
+       }
+
+       public String getLocale() {
+               return locale;
+       }
+
+       public void setLocale(String locale) {
+               this.locale = locale;
+       }
+
+       @Override
+       public boolean hasRole(String name) {
+               return roles.contains(name);
+       }
+
+       @Override
+       public String[] getRoles() {
+               return roles.toArray(new String[roles.size()]);
+       }
+
+       public void setRoles(String[] roles) {
+               this.roles = Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles)));
+       }
+
+       @Override
+       public int hashCode() {
+               return cmsSessionId != null ? cmsSessionId.hashCode() : super.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return displayName != null ? displayName : name != null ? name : super.toString();
+       }
+
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsTokenServlet.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/CmsTokenServlet.java
new file mode 100644 (file)
index 0000000..983202a
--- /dev/null
@@ -0,0 +1,117 @@
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.time.ZonedDateTime;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.cms.CmsUserManager;
+import org.argeo.cms.auth.RemoteAuthCallback;
+import org.argeo.cms.auth.RemoteAuthCallbackHandler;
+import org.argeo.cms.servlet.ServletHttpRequest;
+import org.argeo.cms.servlet.ServletHttpResponse;
+import org.argeo.util.naming.NamingUtils;
+import org.osgi.service.useradmin.Authorization;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Provides access to tokens. */
+public class CmsTokenServlet extends HttpServlet {
+       private static final long serialVersionUID = 302918711430864140L;
+
+       public final static String PARAM_EXPIRY_DATE = "expiryDate";
+       public final static String PARAM_TOKEN = "token";
+
+       private final static int DEFAULT_HOURS = 24;
+
+       private CmsUserManager userManager;
+       private ObjectMapper objectMapper = new ObjectMapper();
+
+       @Override
+       protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               ServletHttpRequest request = new ServletHttpRequest(req);
+               ServletHttpResponse response = new ServletHttpResponse(resp);
+               LoginContext lc = null;
+               try {
+                       lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(request, response) {
+                               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                                       for (Callback callback : callbacks) {
+                                               if (callback instanceof RemoteAuthCallback) {
+                                                       ((RemoteAuthCallback) callback).setRequest(request);
+                                                       ((RemoteAuthCallback) callback).setResponse(response);
+                                               }
+                                       }
+                               }
+                       });
+                       lc.login();
+               } catch (LoginException e) {
+                       // ignore
+               }
+
+               try {
+                       Subject subject = lc.getSubject();
+                       Authorization authorization = extractFrom(subject.getPrivateCredentials(Authorization.class));
+                       String token = UUID.randomUUID().toString();
+                       String expiryDateStr = req.getParameter(PARAM_EXPIRY_DATE);
+                       ZonedDateTime expiryDate;
+                       if (expiryDateStr != null) {
+                               expiryDate = NamingUtils.ldapDateToZonedDateTime(expiryDateStr);
+                       } else {
+                               expiryDate = ZonedDateTime.now().plusHours(DEFAULT_HOURS);
+                               expiryDateStr = NamingUtils.instantToLdapDate(expiryDate);
+                       }
+                       userManager.addAuthToken(authorization.getName(), token, expiryDate);
+
+                       TokenDescriptor tokenDescriptor = new TokenDescriptor();
+                       tokenDescriptor.setUsername(authorization.getName());
+                       tokenDescriptor.setToken(token);
+                       tokenDescriptor.setExpiryDate(expiryDateStr);
+//                     tokenDescriptor.setRoles(Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles))));
+
+                       resp.setContentType("application/json");
+                       JsonGenerator jg = objectMapper.getFactory().createGenerator(resp.getWriter());
+                       jg.writeObject(tokenDescriptor);
+               } catch (Exception e) {
+                       new CmsExceptionsChain(e).writeAsJson(objectMapper, resp);
+               }
+       }
+
+       @Override
+       protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               // temporarily wrap POST for ease of testing
+               doPost(req, resp);
+       }
+
+       @Override
+       protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               try {
+                       String token = req.getParameter(PARAM_TOKEN);
+                       userManager.expireAuthToken(token);
+               } catch (Exception e) {
+                       new CmsExceptionsChain(e).writeAsJson(objectMapper, resp);
+               }
+       }
+
+       protected <T> T extractFrom(Set<T> creds) {
+               if (creds.size() > 0)
+                       return creds.iterator().next();
+               else
+                       return null;
+       }
+
+       public void setUserManager(CmsUserManager userManager) {
+               this.userManager = userManager;
+       }
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/TokenDescriptor.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/TokenDescriptor.java
new file mode 100644 (file)
index 0000000..1541b4f
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.cms.integration;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/** A serializable descriptor of a token. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class TokenDescriptor implements Serializable {
+       private static final long serialVersionUID = -6607393871416803324L;
+
+       private String token;
+       private String username;
+       private String expiryDate;
+//     private Set<String> roles;
+
+       public String getToken() {
+               return token;
+       }
+
+       public void setToken(String token) {
+               this.token = token;
+       }
+
+       public String getUsername() {
+               return username;
+       }
+
+       public void setUsername(String username) {
+               this.username = username;
+       }
+
+//     public Set<String> getRoles() {
+//             return roles;
+//     }
+//
+//     public void setRoles(Set<String> roles) {
+//             this.roles = roles;
+//     }
+
+       public String getExpiryDate() {
+               return expiryDate;
+       }
+
+       public void setExpiryDate(String expiryDate) {
+               this.expiryDate = expiryDate;
+       }
+
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/package-info.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/integration/package-info.java
new file mode 100644 (file)
index 0000000..1405737
--- /dev/null
@@ -0,0 +1,2 @@
+/** Argeo CMS integration (JSON, web services). */
+package org.argeo.cms.integration;
\ No newline at end of file
index 05de32c482ca1c356119396ffc0eb11ceb3d17cf..f1d9216c2d19fca8ddd7f2ce96ba18cbb7636a62 100644 (file)
@@ -1,19 +1,59 @@
 package org.argeo.cms.servlet.internal.jetty;
 
 import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.websocket.DeploymentException;
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpointConfig;
 
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.websocket.javax.server.CmsWebSocketConfigurator;
+import org.argeo.cms.websocket.javax.server.TestEndpoint;
+import org.argeo.util.LangUtils;
 import org.eclipse.equinox.http.jetty.JettyConfigurator;
+import org.osgi.framework.BundleContext;
 import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
 import org.osgi.service.cm.ConfigurationException;
 import org.osgi.service.cm.ManagedServiceFactory;
+import org.osgi.util.tracker.ServiceTracker;
 
 public class JettyServiceFactory implements ManagedServiceFactory {
-       private final CmsLog log = CmsLog.getLog(JettyServiceFactory.class);
+       private final static CmsLog log = CmsLog.getLog(JettyServiceFactory.class);
+
+       final static String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer";
+       // Argeo specific
+       final static String WEBSOCKET_ENABLED = "websocket.enabled";
+
+       private final BundleContext bc = FrameworkUtil.getBundle(JettyServiceFactory.class).getBundleContext();
 
        public void start() {
+               ServiceTracker<ServerContainer, ServerContainer> serverSt = new ServiceTracker<ServerContainer, ServerContainer>(
+                               bc, ServerContainer.class, null) {
+
+                       @Override
+                       public ServerContainer addingService(ServiceReference<ServerContainer> reference) {
+                               ServerContainer serverContainer = super.addingService(reference);
+
+                               BundleContext bc = reference.getBundle().getBundleContext();
+                               ServiceReference<ServerEndpointConfig.Configurator> srConfigurator = bc
+                                               .getServiceReference(ServerEndpointConfig.Configurator.class);
+                               ServerEndpointConfig.Configurator endpointConfigurator = bc.getService(srConfigurator);
+                               ServerEndpointConfig config = ServerEndpointConfig.Builder
+                                               .create(TestEndpoint.class, "/ws/test/events/").configurator(endpointConfigurator).build();
+                               try {
+                                       serverContainer.addEndpoint(config);
+                               } catch (DeploymentException e) {
+                                       throw new IllegalStateException("Cannot initalise the WebSocket server runtime.", e);
+                               }
+                               return serverContainer;
+                       }
 
+               };
+               serverSt.open();
        }
 
        @Override
@@ -25,24 +65,24 @@ public class JettyServiceFactory implements ManagedServiceFactory {
        public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException {
                // Explicitly configures Jetty so that the default server is not started by the
                // activator of the Equinox Jetty bundle.
+               Map<String, String> config = LangUtils.dictToStringMap(properties);
+               if (!config.isEmpty()) {
+                       config.put("customizer.class", CMS_JETTY_CUSTOMIZER_CLASS);
 
-//             if (!webServerConfig.isEmpty()) {
-//             webServerConfig.put("customizer.class", KernelConstants.CMS_JETTY_CUSTOMIZER_CLASS);
-//
-//             // TODO centralise with Jetty extender
-//             Object webSocketEnabled = webServerConfig.get(InternalHttpConstants.WEBSOCKET_ENABLED);
-//             if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) {
-//                     bc.registerService(ServerEndpointConfig.Configurator.class, new CmsWebSocketConfigurator(), null);
-//                     webServerConfig.put(InternalHttpConstants.WEBSOCKET_ENABLED, "true");
-//             }
-//     }
+                       // TODO centralise with Jetty extender
+                       Object webSocketEnabled = config.get(WEBSOCKET_ENABLED);
+                       if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) {
+                               bc.registerService(ServerEndpointConfig.Configurator.class, new CmsWebSocketConfigurator(), null);
+                               config.put(WEBSOCKET_ENABLED, "true");
+                       }
+               }
 
                int tryCount = 60;
                try {
                        tryGettyJetty: while (tryCount > 0) {
                                try {
                                        // FIXME deal with multiple ids
-                                       JettyConfigurator.startServer(CmsConstants.DEFAULT, properties);
+                                       JettyConfigurator.startServer(CmsConstants.DEFAULT, new Hashtable<>(config));
                                        // Explicitly starts Jetty OSGi HTTP bundle, so that it gets triggered if OSGi
                                        // configuration is not cleaned
                                        FrameworkUtil.getBundle(JettyConfigurator.class).start();
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/CmsWebSocketConfigurator.java
new file mode 100644 (file)
index 0000000..8cc1655
--- /dev/null
@@ -0,0 +1,109 @@
+package org.argeo.cms.websocket.javax.server;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.List;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+import javax.websocket.server.ServerEndpointConfig.Configurator;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.auth.RemoteAuthCallbackHandler;
+import org.argeo.cms.auth.RemoteAuthSession;
+import org.argeo.cms.servlet.ServletHttpSession;
+import org.osgi.service.http.context.ServletContextHelper;
+
+/**
+ * <strong>Disabled until third party issues are solved.</strong>. Customises
+ * the initialisation of a new web socket.
+ */
+public class CmsWebSocketConfigurator extends Configurator {
+       public final static String WEBSOCKET_SUBJECT = "org.argeo.cms.websocket.subject";
+
+       private final static CmsLog log = CmsLog.getLog(CmsWebSocketConfigurator.class);
+       final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+
+       @Override
+       public boolean checkOrigin(String originHeaderValue) {
+               return true;
+       }
+
+       @Override
+       public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
+               try {
+                       return endpointClass.getDeclaredConstructor().newInstance();
+               } catch (Exception e) {
+                       throw new IllegalArgumentException("Cannot get endpoint instance", e);
+               }
+       }
+
+       @Override
+       public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested) {
+               return requested;
+       }
+
+       @Override
+       public String getNegotiatedSubprotocol(List<String> supported, List<String> requested) {
+               if ((requested == null) || (requested.size() == 0))
+                       return "";
+               if ((supported == null) || (supported.isEmpty()))
+                       return "";
+               for (String possible : requested) {
+                       if (possible == null)
+                               continue;
+                       if (supported.contains(possible))
+                               return possible;
+               }
+               return "";
+       }
+
+       @Override
+       public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
+               if(true)
+                       return;
+
+               RemoteAuthSession httpSession = new ServletHttpSession(
+                               (javax.servlet.http.HttpSession) request.getHttpSession());
+               if (log.isDebugEnabled() && httpSession != null)
+                       log.debug("Web socket HTTP session id: " + httpSession.getId());
+
+               if (httpSession == null) {
+                       rejectResponse(response, null);
+               }
+               try {
+                       LoginContext lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_USER, new RemoteAuthCallbackHandler(httpSession));
+                       lc.login();
+                       if (log.isDebugEnabled())
+                               log.debug("Web socket logged-in as " + lc.getSubject());
+                       Subject.doAs(lc.getSubject(), new PrivilegedAction<Void>() {
+
+                               @Override
+                               public Void run() {
+                                       sec.getUserProperties().put(ServletContextHelper.REMOTE_USER, AccessController.getContext());
+                                       return null;
+                               }
+
+                       });
+               } catch (Exception e) {
+                       rejectResponse(response, e);
+               }
+       }
+
+       /**
+        * Behaviour when the web socket could not be authenticated. Throws an
+        * {@link IllegalStateException} by default.
+        * 
+        * @param e can be null
+        */
+       protected void rejectResponse(HandshakeResponse response, Exception e) {
+               // violent implementation, as suggested in
+               // https://stackoverflow.com/questions/21763829/jsr-356-how-to-abort-a-websocket-connection-during-the-handshake
+//             throw new IllegalStateException("Web socket cannot be authenticated");
+       }
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/TestEndpoint.java
new file mode 100644 (file)
index 0000000..e01f6f7
--- /dev/null
@@ -0,0 +1,178 @@
+package org.argeo.cms.websocket.javax.server;
+
+import java.io.IOException;
+import java.security.AccessControlContext;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.integration.CmsExceptionsChain;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
+import org.osgi.service.http.context.ServletContextHelper;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Provides WebSocket access. */
+@ServerEndpoint(value = "/ws/test/events/")
+public class TestEndpoint implements EventHandler {
+       private final static CmsLog log = CmsLog.getLog(TestEndpoint.class);
+
+       final static String TOPICS_BASE = "/test";
+       final static String INPUT = "input";
+       final static String TOPIC = "topic";
+       final static String VIEW_UID = "viewUid";
+       final static String COMPUTATION_UID = "computationUid";
+       final static String MESSAGES = "messages";
+       final static String ERRORS = "errors";
+
+       final static String EXCEPTION = "exception";
+       final static String MESSAGE = "message";
+
+       private BundleContext bc = FrameworkUtil.getBundle(TestEndpoint.class).getBundleContext();
+
+       private String wsSessionId;
+       private RemoteEndpoint.Basic remote;
+       private ServiceRegistration<EventHandler> eventHandlerSr;
+
+       // json
+       private ObjectMapper objectMapper = new ObjectMapper();
+
+       private WebSocketView view;
+
+       @OnOpen
+       public void onWebSocketConnect(Session session) {
+               wsSessionId = session.getId();
+
+               // 24h timeout
+               session.setMaxIdleTimeout(1000 * 60 * 60 * 24);
+
+               Map<String, Object> userProperties = session.getUserProperties();
+               Subject subject = null;
+//             AccessControlContext accessControlContext = (AccessControlContext) userProperties
+//                             .get(ServletContextHelper.REMOTE_USER);
+//             Subject subject = Subject.getSubject(accessControlContext);
+//             // Deal with authentication failure
+//             if (subject == null) {
+//                     try {
+//                             CloseReason.CloseCode closeCode = new CloseReason.CloseCode() {
+//
+//                                     @Override
+//                                     public int getCode() {
+//                                             return 4001;
+//                                     }
+//                             };
+//                             session.close(new CloseReason(closeCode, "Unauthorized"));
+//                             if (log.isTraceEnabled())
+//                                     log.trace("Unauthorized web socket " + wsSessionId + ". Closing with code " + closeCode.getCode()
+//                                                     + ".");
+//                             return;
+//                     } catch (IOException e) {
+//                             // silent
+//                     }
+//                     return;// ignore
+//             }
+
+               if (log.isDebugEnabled())
+                       log.debug("WS#" + wsSessionId + " open for: " + subject);
+               remote = session.getBasicRemote();
+               view = new WebSocketView(subject);
+
+               // OSGi events
+               String[] topics = new String[] { TOPICS_BASE + "/*" };
+               Hashtable<String, Object> ht = new Hashtable<>();
+               ht.put(EventConstants.EVENT_TOPIC, topics);
+               ht.put(EventConstants.EVENT_FILTER, "(" + VIEW_UID + "=" + view.getUid() + ")");
+               eventHandlerSr = bc.registerService(EventHandler.class, this, ht);
+
+               if (log.isDebugEnabled())
+                       log.debug("New view " + view.getUid() + " opened, via web socket.");
+       }
+
+       @OnMessage
+       public void onWebSocketText(Session session, String message) throws JsonMappingException, JsonProcessingException {
+               try {
+                       if (log.isTraceEnabled())
+                               log.trace("WS#" + view.getUid() + " received:\n" + message + "\n");
+//                     JsonNode jsonNode = objectMapper.readTree(message);
+//                     String topic = jsonNode.get(TOPIC).textValue();
+
+                       final String computationUid = null;
+//                     if (MY_TOPIC.equals(topic)) {
+//                             view.checkRole(SPECIFIC_ROLE);
+//                             computationUid= process();
+//                     }
+                       remote.sendText("ACK");
+               } catch (Exception e) {
+                       log.error("Error when receiving web socket message", e);
+                       sendSystemErrorMessage(e);
+               }
+       }
+
+       @OnClose
+       public void onWebSocketClose(CloseReason reason) {
+               if (eventHandlerSr != null)
+                       eventHandlerSr.unregister();
+               if (view != null && log.isDebugEnabled())
+                       log.debug("WS#" + view.getUid() + " closed: " + reason);
+       }
+
+       @OnError
+       public void onWebSocketError(Throwable cause) {
+               if (view != null) {
+                       log.error("WS#" + view.getUid() + " ERROR", cause);
+               } else {
+                       if (log.isTraceEnabled())
+                               log.error("Error in web socket session " + wsSessionId, cause);
+               }
+       }
+
+       @Override
+       public void handleEvent(Event event) {
+               try {
+                       Object uid = event.getProperty(COMPUTATION_UID);
+                       Exception exception = (Exception) event.getProperty(EXCEPTION);
+                       if (exception != null) {
+                               CmsExceptionsChain systemErrors = new CmsExceptionsChain(exception);
+                               String sent = systemErrors.toJsonString(objectMapper);
+                               remote.sendText(sent);
+                               return;
+                       }
+                       String topic = event.getTopic();
+                       if (log.isTraceEnabled())
+                               log.trace("WS#" + view.getUid() + " " + topic + ": notify event " + topic + "#" + uid + ", " + event);
+               } catch (Exception e) {
+                       log.error("Error when handling event for WebSocket", e);
+                       sendSystemErrorMessage(e);
+               }
+
+       }
+
+       /** Sends an error message in JSON format. */
+       protected void sendSystemErrorMessage(Exception e) {
+               CmsExceptionsChain systemErrors = new CmsExceptionsChain(e);
+               try {
+                       if (remote != null)
+                               remote.sendText(systemErrors.toJsonString(objectMapper));
+               } catch (Exception e1) {
+                       log.error("Cannot send WebSocket system error messages " + systemErrors, e1);
+               }
+       }
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketTest.java
new file mode 100644 (file)
index 0000000..819837b
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.cms.websocket.javax.server;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.WebSocket;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+
+/** Tests connectivity to the web socket server. */
+public class WebSocketTest {
+
+       public static void main(String[] args) throws Exception {
+               CompletableFuture<Boolean> received = new CompletableFuture<>();
+               WebSocket.Listener listener = new WebSocket.Listener() {
+
+                       public CompletionStage<?> onText(WebSocket webSocket, CharSequence message, boolean last) {
+                               System.out.println(message);
+                               CompletionStage<String> res = CompletableFuture.completedStage(message.toString());
+                               received.complete(true);
+                               return res;
+                       }
+               };
+
+               HttpClient client = HttpClient.newHttpClient();
+               CompletableFuture<WebSocket> ws = client.newWebSocketBuilder()
+                               .buildAsync(URI.create("ws://localhost:7070/ws/test/events/"), listener);
+               WebSocket webSocket = ws.get();
+               webSocket.sendText("TEST", true);
+
+               received.get(10, TimeUnit.SECONDS);
+               webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "");
+       }
+
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketView.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/WebSocketView.java
new file mode 100644 (file)
index 0000000..a5da88b
--- /dev/null
@@ -0,0 +1,60 @@
+package org.argeo.cms.websocket.javax.server;
+
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Abstraction of a single Frontend view, that is a web browser page. There can
+ * be multiple views within one single authenticated HTTP session.
+ */
+public class WebSocketView {
+       private final String uid;
+       private Subject subject;
+
+       public WebSocketView(Subject subject) {
+               this.uid = UUID.randomUUID().toString();
+               this.subject = subject;
+       }
+
+       public String getUid() {
+               return uid;
+       }
+
+       public Set<String> getRoles() {
+               return roles(subject);
+       }
+
+       public boolean isInRole(String role) {
+               return getRoles().contains(role);
+       }
+
+       public void checkRole(String role) {
+               checkRole(subject, role);
+       }
+
+       public final static Set<String> roles(Subject subject) {
+               Set<String> roles = new HashSet<String>();
+               X500Principal principal = subject.getPrincipals(X500Principal.class).iterator().next();
+               String username = principal.getName();
+               roles.add(username);
+               for (Principal group : subject.getPrincipals()) {
+                       if (group instanceof Role)
+                               roles.add(group.getName());
+               }
+               return roles;
+       }
+
+       public static void checkRole(Subject subject, String role) {
+               Set<String> roles = roles(subject);
+               if (!roles.contains(role))
+                       throw new IllegalStateException("User is not in role " + role);
+       }
+
+}
diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/package-info.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/websocket/javax/server/package-info.java
new file mode 100644 (file)
index 0000000..564c881
--- /dev/null
@@ -0,0 +1,2 @@
+/** Argeo CMS websocket integration. */
+package org.argeo.cms.websocket.javax.server;
\ No newline at end of file
diff --git a/eclipse/org.argeo.ext.equinox.jetty/.classpath b/eclipse/org.argeo.ext.equinox.jetty/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/eclipse/org.argeo.ext.equinox.jetty/.gitignore b/eclipse/org.argeo.ext.equinox.jetty/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/eclipse/org.argeo.ext.equinox.jetty/.project b/eclipse/org.argeo.ext.equinox.jetty/.project
new file mode 100644 (file)
index 0000000..0b9700d
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.ext.equinox.jetty</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/eclipse/org.argeo.ext.equinox.jetty/META-INF/.gitignore b/eclipse/org.argeo.ext.equinox.jetty/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/eclipse/org.argeo.ext.equinox.jetty/bnd.bnd b/eclipse/org.argeo.ext.equinox.jetty/bnd.bnd
new file mode 100644 (file)
index 0000000..7fca536
--- /dev/null
@@ -0,0 +1 @@
+Fragment-Host: org.eclipse.equinox.http.jetty
diff --git a/eclipse/org.argeo.ext.equinox.jetty/build.properties b/eclipse/org.argeo.ext.equinox.jetty/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java b/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java
new file mode 100644 (file)
index 0000000..8ad95c9
--- /dev/null
@@ -0,0 +1,40 @@
+package org.argeo.equinox.jetty;
+
+import java.util.Dictionary;
+
+import javax.servlet.ServletContext;
+import javax.websocket.DeploymentException;
+import javax.websocket.server.ServerContainer;
+
+import org.eclipse.equinox.http.jetty.JettyCustomizer;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
+import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer.Configurator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+/** Customises the Jetty HTTP server. */
+public class CmsJettyCustomizer extends JettyCustomizer {
+       private BundleContext bc = FrameworkUtil.getBundle(CmsJettyCustomizer.class).getBundleContext();
+
+       public final static String WEBSOCKET_ENABLED = "websocket.enabled";
+
+       @Override
+       public Object customizeContext(Object context, Dictionary<String, ?> settings) {
+               // WebSocket
+               Object webSocketEnabled = settings.get(WEBSOCKET_ENABLED);
+               if (webSocketEnabled != null && webSocketEnabled.toString().equals("true")) {
+                       ServletContextHandler servletContextHandler = (ServletContextHandler) context;
+                       JavaxWebSocketServletContainerInitializer.configure(servletContextHandler, new Configurator() {
+
+                               @Override
+                               public void accept(ServletContext servletContext, ServerContainer serverContainer)
+                                               throws DeploymentException {
+                                       bc.registerService(javax.websocket.server.ServerContainer.class, serverContainer, null);
+                               }
+                       });
+               }
+               return super.customizeContext(context, settings);
+
+       }
+}
diff --git a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/package-info.java b/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/package-info.java
new file mode 100644 (file)
index 0000000..41c8ce9
--- /dev/null
@@ -0,0 +1,2 @@
+/** Equinox Jetty extensions. */
+package org.argeo.equinox.jetty;
\ No newline at end of file
diff --git a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrReadServlet.java b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrReadServlet.java
new file mode 100644 (file)
index 0000000..b0cd789
--- /dev/null
@@ -0,0 +1,319 @@
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.AccessControlContext;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.security.auth.Subject;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.api.JackrabbitNode;
+import org.apache.jackrabbit.api.JackrabbitValue;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.integration.CmsExceptionsChain;
+import org.argeo.jcr.JcrUtils;
+import org.osgi.service.http.context.ServletContextHelper;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Access a JCR repository via web services. */
+public class JcrReadServlet extends HttpServlet {
+       private static final long serialVersionUID = 6536175260540484539L;
+       private final static CmsLog log = CmsLog.getLog(JcrReadServlet.class);
+
+       protected final static String ACCEPT_HTTP_HEADER = "Accept";
+       protected final static String CONTENT_DISPOSITION_HTTP_HEADER = "Content-Disposition";
+
+       protected final static String OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
+       protected final static String XML_CONTENT_TYPE = "application/xml";
+       protected final static String JSON_CONTENT_TYPE = "application/json";
+
+       private final static String PARAM_VERBOSE = "verbose";
+       private final static String PARAM_DEPTH = "depth";
+
+       protected final static String JCR_NODES = "jcr:nodes";
+       // cf. javax.jcr.Property
+       protected final static String JCR_PATH = "path";
+       protected final static String JCR_NAME = "name";
+
+       protected final static String _JCR = "_jcr";
+       protected final static String JCR_PREFIX = "jcr:";
+       protected final static String REP_PREFIX = "rep:";
+
+       private Repository repository;
+       private Integer maxDepth = 8;
+
+       private ObjectMapper objectMapper = new ObjectMapper();
+
+       @Override
+       protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               if (log.isTraceEnabled())
+                       log.trace("Data service: " + req.getPathInfo());
+
+               String dataWorkspace = getWorkspace(req);
+               String jcrPath = getJcrPath(req);
+
+               boolean verbose = req.getParameter(PARAM_VERBOSE) != null && !req.getParameter(PARAM_VERBOSE).equals("false");
+               int depth = 1;
+               if (req.getParameter(PARAM_DEPTH) != null) {
+                       depth = Integer.parseInt(req.getParameter(PARAM_DEPTH));
+                       if (depth > maxDepth)
+                               throw new RuntimeException("Depth " + depth + " is higher than maximum " + maxDepth);
+               }
+
+               Session session = null;
+               try {
+                       // authentication
+                       session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+                       if (!session.itemExists(jcrPath))
+                               throw new RuntimeException("JCR node " + jcrPath + " does not exist");
+                       Node node = session.getNode(jcrPath);
+
+                       List<String> acceptHeader = readAcceptHeader(req);
+                       if (!acceptHeader.isEmpty() && node.isNodeType(NodeType.NT_FILE)) {
+                               resp.setContentType(OCTET_STREAM_CONTENT_TYPE);
+                               resp.addHeader(CONTENT_DISPOSITION_HTTP_HEADER, "attachment; filename='" + node.getName() + "'");
+                               IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream());
+                               resp.flushBuffer();
+                       } else {
+                               if (!acceptHeader.isEmpty() && acceptHeader.get(0).equals(XML_CONTENT_TYPE)) {
+                                       // TODO Use req.startAsync(); ?
+                                       resp.setContentType(XML_CONTENT_TYPE);
+                                       session.exportSystemView(node.getPath(), resp.getOutputStream(), false, depth <= 1);
+                                       return;
+                               }
+                               if (!acceptHeader.isEmpty() && !acceptHeader.contains(JSON_CONTENT_TYPE)) {
+                                       if (log.isTraceEnabled())
+                                               log.warn("Content type " + acceptHeader + " in Accept header is not supported. Supported: "
+                                                               + JSON_CONTENT_TYPE + " (default), " + XML_CONTENT_TYPE);
+                               }
+                               resp.setContentType(JSON_CONTENT_TYPE);
+                               JsonGenerator jsonGenerator = getObjectMapper().getFactory().createGenerator(resp.getWriter());
+                               jsonGenerator.writeStartObject();
+                               writeNodeChildren(node, jsonGenerator, depth, verbose);
+                               writeNodeProperties(node, jsonGenerator, verbose);
+                               jsonGenerator.writeEndObject();
+                               jsonGenerator.flush();
+                       }
+               } catch (Exception e) {
+                       new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       protected Session openJcrSession(HttpServletRequest req, HttpServletResponse resp, Repository repository,
+                       String workspace) throws RepositoryException {
+               AccessControlContext acc = (AccessControlContext) req.getAttribute(ServletContextHelper.REMOTE_USER);
+               Subject subject = Subject.getSubject(acc);
+               try {
+                       return Subject.doAs(subject, new PrivilegedExceptionAction<Session>() {
+
+                               @Override
+                               public Session run() throws RepositoryException {
+                                       return repository.login(workspace);
+                               }
+
+                       });
+               } catch (PrivilegedActionException e) {
+                       if (e.getException() instanceof RepositoryException)
+                               throw (RepositoryException) e.getException();
+                       else
+                               throw new RuntimeException(e.getException());
+               }
+//             return workspace != null ? repository.login(workspace) : repository.login();
+       }
+
+       protected String getWorkspace(HttpServletRequest req) {
+               String path = req.getPathInfo();
+               try {
+                       path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
+               } catch (UnsupportedEncodingException e) {
+                       throw new IllegalArgumentException(e);
+               }
+               String[] pathTokens = path.split("/");
+               return pathTokens[1];
+       }
+
+       protected String getJcrPath(HttpServletRequest req) {
+               String path = req.getPathInfo();
+               try {
+                       path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
+               } catch (UnsupportedEncodingException e) {
+                       throw new IllegalArgumentException(e);
+               }
+               String[] pathTokens = path.split("/");
+               String domain = pathTokens[1];
+               String jcrPath = path.substring(domain.length() + 1);
+               return jcrPath;
+       }
+
+       protected List<String> readAcceptHeader(HttpServletRequest req) {
+               List<String> lst = new ArrayList<>();
+               String acceptHeader = req.getHeader(ACCEPT_HTTP_HEADER);
+               if (acceptHeader == null)
+                       return lst;
+//             Enumeration<String> acceptHeader = req.getHeaders(ACCEPT_HTTP_HEADER);
+//             while (acceptHeader.hasMoreElements()) {
+               String[] arr = acceptHeader.split("\\.");
+               for (int i = 0; i < arr.length; i++) {
+                       String str = arr[i].trim();
+                       if (!"".equals(str))
+                               lst.add(str);
+               }
+//             }
+               return lst;
+       }
+
+       protected void writeNodeProperties(Node node, JsonGenerator jsonGenerator, boolean verbose)
+                       throws RepositoryException, IOException {
+               String jcrPath = node.getPath();
+               Map<String, Map<String, Property>> namespaces = new TreeMap<>();
+
+               PropertyIterator pit = node.getProperties();
+               properties: while (pit.hasNext()) {
+                       Property property = pit.nextProperty();
+
+                       final String propertyName = property.getName();
+                       int columnIndex = propertyName.indexOf(':');
+                       if (columnIndex > 0) {
+                               // mark prefix with a '_' before the name of the object, according to JSON
+                               // conventions to indicate a special value
+                               String prefix = "_" + propertyName.substring(0, columnIndex);
+                               String unqualifiedName = propertyName.substring(columnIndex + 1);
+                               if (!namespaces.containsKey(prefix))
+                                       namespaces.put(prefix, new LinkedHashMap<String, Property>());
+                               Map<String, Property> map = namespaces.get(prefix);
+                               assert !map.containsKey(unqualifiedName);
+                               map.put(unqualifiedName, property);
+                               continue properties;
+                       }
+
+                       if (property.getType() == PropertyType.BINARY) {
+                               if (!(node instanceof JackrabbitNode)) {
+                                       continue properties;// skip
+                               }
+                       }
+
+                       writeProperty(propertyName, property, jsonGenerator);
+               }
+
+               for (String prefix : namespaces.keySet()) {
+                       Map<String, Property> map = namespaces.get(prefix);
+                       jsonGenerator.writeFieldName(prefix);
+                       jsonGenerator.writeStartObject();
+                       if (_JCR.equals(prefix)) {
+                               jsonGenerator.writeStringField(JCR_NAME, node.getName());
+                               jsonGenerator.writeStringField(JCR_PATH, jcrPath);
+                       }
+                       properties: for (String unqualifiedName : map.keySet()) {
+                               Property property = map.get(unqualifiedName);
+                               if (property.getType() == PropertyType.BINARY) {
+                                       if (!(node instanceof JackrabbitNode)) {
+                                               continue properties;// skip
+                                       }
+                               }
+                               writeProperty(unqualifiedName, property, jsonGenerator);
+                       }
+                       jsonGenerator.writeEndObject();
+               }
+       }
+
+       protected void writeProperty(String fieldName, Property property, JsonGenerator jsonGenerator)
+                       throws RepositoryException, IOException {
+               if (!property.isMultiple()) {
+                       jsonGenerator.writeFieldName(fieldName);
+                       writePropertyValue(property.getType(), property.getValue(), jsonGenerator);
+               } else {
+                       jsonGenerator.writeFieldName(fieldName);
+                       jsonGenerator.writeStartArray();
+                       Value[] values = property.getValues();
+                       for (Value value : values) {
+                               writePropertyValue(property.getType(), value, jsonGenerator);
+                       }
+                       jsonGenerator.writeEndArray();
+               }
+       }
+
+       protected void writePropertyValue(int type, Value value, JsonGenerator jsonGenerator)
+                       throws RepositoryException, IOException {
+               if (type == PropertyType.DOUBLE)
+                       jsonGenerator.writeNumber(value.getDouble());
+               else if (type == PropertyType.LONG)
+                       jsonGenerator.writeNumber(value.getLong());
+               else if (type == PropertyType.BINARY) {
+                       if (value instanceof JackrabbitValue) {
+                               String contentIdentity = ((JackrabbitValue) value).getContentIdentity();
+                               jsonGenerator.writeString("SHA256:" + contentIdentity);
+                       } else {
+                               // TODO write Base64 ?
+                               jsonGenerator.writeNull();
+                       }
+               } else
+                       jsonGenerator.writeString(value.getString());
+       }
+
+       protected void writeNodeChildren(Node node, JsonGenerator jsonGenerator, int depth, boolean verbose)
+                       throws RepositoryException, IOException {
+               if (!node.hasNodes())
+                       return;
+               if (depth <= 0)
+                       return;
+               NodeIterator nit;
+
+               nit = node.getNodes();
+               children: while (nit.hasNext()) {
+                       Node child = nit.nextNode();
+                       if (!verbose && child.getName().startsWith(REP_PREFIX)) {
+                               continue children;// skip Jackrabbit auth metadata
+                       }
+
+                       jsonGenerator.writeFieldName(child.getName());
+                       jsonGenerator.writeStartObject();
+                       writeNodeChildren(child, jsonGenerator, depth - 1, verbose);
+                       writeNodeProperties(child, jsonGenerator, verbose);
+                       jsonGenerator.writeEndObject();
+               }
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setMaxDepth(Integer maxDepth) {
+               this.maxDepth = maxDepth;
+       }
+
+       protected Repository getRepository() {
+               return repository;
+       }
+
+       protected ObjectMapper getObjectMapper() {
+               return objectMapper;
+       }
+
+}
diff --git a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrWriteServlet.java b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrWriteServlet.java
new file mode 100644 (file)
index 0000000..459a1e4
--- /dev/null
@@ -0,0 +1,92 @@
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.io.IOException;
+
+import javax.jcr.ImportUUIDBehavior;
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.integration.CmsExceptionsChain;
+import org.argeo.jcr.JcrUtils;
+
+/** Access a JCR repository via web services. */
+public class JcrWriteServlet extends JcrReadServlet {
+       private static final long serialVersionUID = 17272653843085492L;
+       private final static CmsLog log = CmsLog.getLog(JcrWriteServlet.class);
+
+       @Override
+       protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               if (log.isDebugEnabled())
+                       log.debug("Data service POST: " + req.getPathInfo());
+
+               String dataWorkspace = getWorkspace(req);
+               String jcrPath = getJcrPath(req);
+
+               Session session = null;
+               try {
+                       // authentication
+                       session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+
+                       if (req.getContentType() != null && req.getContentType().equals(XML_CONTENT_TYPE)) {
+//                             resp.setContentType(XML_CONTENT_TYPE);
+                               session.getWorkspace().importXML(jcrPath, req.getInputStream(),
+                                               ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
+                               return;
+                       }
+
+                       if (!session.itemExists(jcrPath)) {
+                               String parentPath = FilenameUtils.getFullPathNoEndSeparator(jcrPath);
+                               String fileName = FilenameUtils.getName(jcrPath);
+                               Node folderNode = JcrUtils.mkfolders(session, parentPath);
+                               byte[] bytes = IOUtils.toByteArray(req.getInputStream());
+                               JcrUtils.copyBytesAsFile(folderNode, fileName, bytes);
+                       } else {
+                               Node node = session.getNode(jcrPath);
+                               if (!node.isNodeType(NodeType.NT_FILE))
+                                       throw new IllegalArgumentException("Node " + jcrPath + " exists but is not a file");
+                               byte[] bytes = IOUtils.toByteArray(req.getInputStream());
+                               JcrUtils.copyBytesAsFile(node.getParent(), node.getName(), bytes);
+                       }
+                       session.save();
+               } catch (Exception e) {
+                       new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       @Override
+       protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               if (log.isDebugEnabled())
+                       log.debug("Data service DELETE: " + req.getPathInfo());
+
+               String dataWorkspace = getWorkspace(req);
+               String jcrPath = getJcrPath(req);
+
+               Session session = null;
+               try {
+                       // authentication
+                       session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+                       if (!session.itemExists(jcrPath)) {
+                               // ignore
+                               return;
+                       } else {
+                               Node node = session.getNode(jcrPath);
+                               node.remove();
+                       }
+                       session.save();
+               } catch (Exception e) {
+                       new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+}