You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2018/10/15 20:37:00 UTC

[isis] branch v2 updated: ISIS-2006: initial prototype

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch v2
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/v2 by this push:
     new 1310040  ISIS-2006: initial prototype
1310040 is described below

commit 13100407dbc33f3cb8108cad1b4f546bc757b7f4
Author: Andi Huber <ah...@apache.org>
AuthorDate: Mon Oct 15 22:35:56 2018 +0200

    ISIS-2006: initial prototype
    
    Task-Url: https://issues.apache.org/jira/browse/ISIS-2006
---
 .../applib/client/ActionParameterListBuilder.java  | 102 +++++++++
 .../apache/isis/applib/client/ResponseDigest.java  | 174 +++++++++++++++
 .../apache/isis/applib/client/RestfulClient.java   | 237 +++++++++++++++++++++
 .../isis/applib/client/RestfulClientConfig.java    |  87 ++++++++
 .../isis/applib/client/RestfulClientException.java |  40 ++++
 .../apache/isis/applib/client/SuppressionType.java |  88 ++++++++
 .../isis/applib/client/auth/BasicAuthFilter.java   |  95 +++++++++
 .../isis/commons/internal/base/_Strings.java       |  18 ++
 .../ContentNegotiationServiceOrgApacheIsisV1.java  |  50 +----
 9 files changed, 843 insertions(+), 48 deletions(-)

diff --git a/core/applib/src/main/java/org/apache/isis/applib/client/ActionParameterListBuilder.java b/core/applib/src/main/java/org/apache/isis/applib/client/ActionParameterListBuilder.java
new file mode 100644
index 0000000..2ed05be
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/client/ActionParameterListBuilder.java
@@ -0,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.applib.client;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.client.Entity;
+
+/**
+ * 
+ * @since 2.0.0-M2
+ */
+public class ActionParameterListBuilder {
+
+    private final Map<String, String> actionParameters = new LinkedHashMap<>();
+    
+    public ActionParameterListBuilder addActionParameter(String parameterName, String parameterValue) {
+        actionParameters.put(parameterName, parameterValue != null 
+                ? value("\"" + parameterValue + "\"") 
+                        : value(JSON_NULL_LITERAL));
+        return this;
+    }
+    
+    public ActionParameterListBuilder addActionParameter(String parameterName, int parameterValue) {
+        actionParameters.put(parameterName, value(""+parameterValue));
+        return this;
+    }
+
+    public ActionParameterListBuilder addActionParameter(String parameterName, long parameterValue) {
+        actionParameters.put(parameterName, value(""+parameterValue));
+        return this;
+    }
+    
+    public ActionParameterListBuilder addActionParameter(String parameterName, byte parameterValue) {
+        actionParameters.put(parameterName, value(""+parameterValue));
+        return this;
+    }
+    
+    public ActionParameterListBuilder addActionParameter(String parameterName, short parameterValue) {
+        actionParameters.put(parameterName, value(""+parameterValue));
+        return this;
+    }
+    
+    public ActionParameterListBuilder addActionParameter(String parameterName, double parameterValue) {
+        actionParameters.put(parameterName, value(""+parameterValue));
+        return this;
+    }
+    
+    public ActionParameterListBuilder addActionParameter(String parameterName, float parameterValue) {
+        actionParameters.put(parameterName, value(""+parameterValue));
+        return this;
+    }
+    
+    public ActionParameterListBuilder addActionParameter(String parameterName, boolean parameterValue) {
+        actionParameters.put(parameterName, value(""+parameterValue));
+        return this;
+    }
+    
+    public Entity<String> build() {
+        
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n")
+        .append(actionParameters.entrySet().stream()
+                .map(this::toJson)
+                .collect(Collectors.joining(",\n")))
+        .append("\n}");
+        
+        return Entity.json(sb.toString());
+    }
+    
+    // -- HELPER
+    
+    private final static String JSON_NULL_LITERAL = "null";
+
+    private String value(String valueLiteral) {
+        return "{\"value\" : " + valueLiteral + "}";
+    }
+    
+    private String toJson(Map.Entry<String, String> entry) {
+        return "   \""+entry.getKey()+"\": "+entry.getValue();
+    }
+   
+    
+}
diff --git a/core/applib/src/main/java/org/apache/isis/applib/client/ResponseDigest.java b/core/applib/src/main/java/org/apache/isis/applib/client/ResponseDigest.java
new file mode 100644
index 0000000..9623c8c
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/client/ResponseDigest.java
@@ -0,0 +1,174 @@
+/*
+ *  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.applib.client;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Future;
+import java.util.function.Function;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status.Family;
+
+import org.apache.isis.commons.internal.base._Strings;
+
+/**
+ * 
+ * @since 2.0.0-M2
+ */
+public class ResponseDigest<T> {
+
+    /** synchronous response processing */
+    public static <T> ResponseDigest<T> of(Response response, Class<T> entityType) {
+        return new ResponseDigest<>(response, entityType).digest();
+    }
+    
+    /** a-synchronous response failure processing */
+    public static <T> ResponseDigest<T> ofAsyncFailure(
+            Future<Response> asyncResponse, 
+            Class<T> entityType, 
+            Exception failure) {
+
+        Response response;
+        try {
+            response = asyncResponse.isDone() ? asyncResponse.get() : null;
+        } catch (Exception e) {
+            response = null;
+        }
+        
+        final ResponseDigest<T> failureDigest = new ResponseDigest<>(response, entityType);
+        return failureDigest.digestAsyncFailure(asyncResponse.isCancelled(), failure);
+    }
+    
+    private final Response response;
+    private final Class<T> entityType;
+    
+    private T entity;
+    private Exception failureCause;
+    
+    
+    protected ResponseDigest(Response response, Class<T> entityType) {
+        this.response = response;
+        this.entityType = entityType;
+    }
+    
+    public boolean isSuccess() {
+        return !isFailure();
+    }
+    
+    public boolean isFailure() {
+        return failureCause!=null;
+    }
+    
+    public T get(){
+        return entity;
+    }
+    
+    public Exception getFailureCause(){
+        return failureCause;
+    }
+    
+    public T ifSuccessGetOrElseMap(Function<Exception, T> failureMapper) {
+        return isSuccess() 
+                ? get()
+                        : failureMapper.apply(getFailureCause());
+    }
+    
+    public <X> X ifSuccessMapOrElseMap(Function<T, X> successMapper, Function<Exception, X> failureMapper) {
+        return isSuccess() 
+                ? successMapper.apply(get())
+                        : failureMapper.apply(getFailureCause());
+    }
+    
+    // -- HELPER
+    
+    private ResponseDigest<T> digest() {
+        
+        if(response==null) {
+            entity = null;
+            failureCause = new NoSuchElementException();
+            return this;
+        }
+        
+        if(!response.hasEntity()) {
+            entity = null;
+            failureCause = new NoSuchElementException(defaultFailureMessage(response));
+            return this;
+        }
+        
+        if(response.getStatusInfo().getFamily() != Family.SUCCESSFUL) {
+            entity = null;
+            failureCause = new RestfulClientException(defaultFailureMessage(response));
+            return this;
+        }
+        
+        try {
+            entity = response.readEntity(entityType);
+        } catch (Exception e) {
+            entity = null;
+            failureCause = new RestfulClientException("failed to read JAX-RS response content", e);
+        }
+        
+        return this;
+    }
+    
+    private ResponseDigest<T> digestAsyncFailure(boolean isCancelled, Exception failure) {
+
+        entity = null;
+
+        
+        if(isCancelled) {
+            failureCause = new RestfulClientException("Async JAX-RS request was canceled", failure);
+            return this;
+        }
+        
+        if(response==null) {
+            failureCause = new RestfulClientException("Async JAX-RS request failed", failure);
+            return this;
+        }
+        
+        failureCause = new RestfulClientException("Async JAX-RS request failed " 
+                + defaultFailureMessage(response), failure);
+        return this;
+        
+    }
+    
+    private String defaultFailureMessage(Response response) {
+        String failureMessage = "non-successful JAX-RS response: " + 
+                String.format("%s (Http-Status-Code: %d)", 
+                        response.getStatusInfo().getReasonPhrase(),
+                        response.getStatus());
+        
+        if(response.hasEntity()) {
+            try {
+                String jsonContent = _Strings.read((InputStream) response.getEntity(), StandardCharsets.UTF_8);
+                return failureMessage + "\nContent:\n" + jsonContent;
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        
+        return failureMessage;
+    }
+
+
+    
+    
+}
diff --git a/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClient.java b/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClient.java
new file mode 100644
index 0000000..a96cbae
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClient.java
@@ -0,0 +1,237 @@
+/*
+ *  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.applib.client;
+
+import static org.apache.isis.commons.internal.base._NullSafe.stream;
+
+import java.util.EnumSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Invocation.Builder;
+import javax.ws.rs.core.Response;
+
+import org.apache.isis.applib.client.auth.BasicAuthFilter;
+import org.apache.isis.applib.client.auth.BasicAuthFilter.Credentials;
+import org.apache.isis.commons.internal.base._Strings;
+import org.apache.isis.commons.internal.context._Context;
+
+/**
+ * Setup the Restful Client with Basic-Auth:
+ * <pre>
+RestfulClientConfig clientConfig = new RestfulClientConfig();
+clientConfig.setRestfulBase("http://localhost:8080/helloworld/restful/");
+// setup basic-auth
+clientConfig.setUseBasicAuth(true);
+clientConfig.setRestfulAuthUser("sven");
+clientConfig.setRestfulAuthPassword("pass");
+
+RestfulClient client = RestfulClient.ofConfig(clientConfig);
+ * </pre>
+ * 
+ * Synchronous example:
+ * <pre>
+
+Builder request = client.request(
+                "services/myService/actions/lookupMyObjectById/invoke", 
+                SuppressionType.setOf(SuppressionType.RO));
+
+Entity<String> args = client.arguments()
+        .addActionParameter("id", "12345")
+        .build();
+
+Response response = request.post(args);
+
+ResponseDigest<MyObject> digest = client.digest(response, MyObject.class);
+
+if(digest.isSuccess()) {
+    System.out.println("result: "+ digest.get().get$$instanceId());
+} else {
+    digest.getFailureCause().printStackTrace();
+}
+ * </pre>
+ * Asynchronous example:
+ * <pre>
+ 
+Builder request = client.request(
+                "services/myService/actions/lookupMyObjectById/invoke", 
+                SuppressionType.setOf(SuppressionType.RO));
+
+Entity<String> args = client.arguments()
+        .addActionParameter("id", "12345")
+        .build();
+
+Response response = request
+        .async()
+        .post(args);
+
+CompletableFuture<ResponseDigest<MyObject>> digestFuture = 
+                client.digest(response, MyObject.class);
+        
+ResponseDigest<MyObject> digest = digestFuture.get(); // blocking
+
+if(digest.isSuccess()) {
+    System.out.println("result: "+ digest.get().get$$instanceId());
+} else {
+    digest.getFailureCause().printStackTrace();
+}
+ * </pre>
+ * 
+ * Maven Setup:
+ * <pre>{@code
+<dependency>
+    <groupId>org.apache.isis.core</groupId>
+    <artifactId>isis-core-applib</artifactId>
+    <version>2.0.0-M2-SNAPSHOT</version>
+</dependency>
+<dependency>
+    <groupId>javax.ws.rs</groupId>
+    <artifactId>javax.ws.rs-api</artifactId>
+    <version>2.1.1</version>
+</dependency>
+<dependency>
+    <groupId>org.glassfish.jersey.core</groupId>
+    <artifactId>jersey-client</artifactId>
+    <version>2.25.1</version>
+</dependency>
+<dependency>
+    <groupId>org.eclipse.persistence</groupId>
+    <artifactId>org.eclipse.persistence.moxy</artifactId>
+    <version>2.6.0</version>
+</dependency>} 
+ * </pre>
+ * 
+ * @since 2.0.0-M2
+ */
+public class RestfulClient {
+    
+    public static String DEFAULT_RESPONSE_CONTENT_TYPE = "application/json;profile=urn:org.apache.isis/v1";
+
+    private RestfulClientConfig clientConfig;
+    private Client client;
+    
+    public static RestfulClient ofConfig(RestfulClientConfig clientConfig) {
+        RestfulClient restClient = new RestfulClient();
+        restClient.init(clientConfig);
+        return restClient;
+    }
+    
+    public void init(RestfulClientConfig clientConfig) {
+        this.clientConfig = clientConfig;
+        client = ClientBuilder.newClient();
+        
+        if(clientConfig.isUseBasicAuth()){
+            final Credentials credentials = Credentials.of(
+                    clientConfig.getRestfulAuthUser(), 
+                    clientConfig.getRestfulAuthPassword());
+            client.register(BasicAuthFilter.of(credentials));
+        }
+        
+        try {
+            Class<?> MOXyJsonProvider = _Context.loadClass("org.eclipse.persistence.jaxb.rs.MOXyJsonProvider");
+            client.register(MOXyJsonProvider);
+        } catch (Exception e) {
+            // this is just provided for convenience
+        }
+        
+    }
+    
+    public RestfulClientConfig getConfig() {
+        return clientConfig;
+    }
+    
+    public Client getJaxRsClient() {
+        return client;
+    }
+    
+    // -- REQUEST BUILDER
+    
+    public Builder request(String path, SuppressionType ... suppressionTypes) {
+        return request(path, SuppressionType.setOf(suppressionTypes));
+    }
+    
+    public Builder request(String path, EnumSet<SuppressionType> suppressionTypes) {
+        final String responseContentType = DEFAULT_RESPONSE_CONTENT_TYPE
+                + toSuppressionLiteral(suppressionTypes);
+
+        return client.target(relativePathToUri(path)).request(responseContentType);
+    }
+
+    // -- ARGUMENT BUILDER
+    
+    public ActionParameterListBuilder arguments() {
+        return new ActionParameterListBuilder();
+    }
+    
+    // -- RESPONSE PROCESSING (SYNC)
+    
+    public <T> ResponseDigest<T> digest(Response response, Class<T> entityType) {
+        return ResponseDigest.of(response, entityType);
+    }
+    
+    // -- RESPONSE PROCESSING (ASYNC)
+    
+    public <T> CompletableFuture<ResponseDigest<T>> digest(
+            final Future<Response> asyncResponse, 
+            final Class<T> entityType) {
+        
+        final CompletableFuture<ResponseDigest<T>> completableFuture = CompletableFuture.supplyAsync(()->{
+            try {
+                Response response = asyncResponse.get();
+                ResponseDigest<T> digest = digest(response, entityType);
+                
+                return digest;
+                
+            } catch (Exception e) {
+                return ResponseDigest.ofAsyncFailure(asyncResponse, entityType, e);
+            }
+        });
+        
+        return completableFuture;
+    }
+    
+    // -- HELPER
+    
+    private String relativePathToUri(String path) {
+        final String baseUri = _Strings.suffix(clientConfig.getRestfulBase(), "/");
+        while(path.startsWith("/")) {
+            path = path.substring(1);
+        }
+        return baseUri + path;
+    }
+    
+    private String toSuppressionLiteral(EnumSet<SuppressionType> suppressionTypes) {
+        final String suppressionSetLiteral = stream(suppressionTypes)
+                .map(SuppressionType::name)
+                .collect(Collectors.joining(","));
+        
+        if(_Strings.isNotEmpty(suppressionSetLiteral)) {
+            return ";suppress=" + suppressionSetLiteral;
+        }
+        
+        return "";
+    }
+
+
+    
+    
+}
diff --git a/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClientConfig.java b/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClientConfig.java
new file mode 100644
index 0000000..c0578b1
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClientConfig.java
@@ -0,0 +1,87 @@
+/*
+ *  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.applib.client;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * 
+ * @since 2.0.0-M2
+ */
+@XmlRootElement(name="restful-client-config")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class RestfulClientConfig {
+
+    // --
+    
+    @XmlElement(name="restfulBase") 
+    private String restfulBase;
+    
+    public String getRestfulBase() {
+        return restfulBase;
+    }
+    
+    public void setRestfulBase(String restfulBase) {
+        this.restfulBase = restfulBase;
+    }
+    
+    // --
+    
+    @XmlElement(name="useBasicAuth") 
+    private boolean useBasicAuth;
+    
+    public boolean isUseBasicAuth() {
+        return useBasicAuth;
+    }
+
+    public void setUseBasicAuth(boolean useBasicAuth) {
+        this.useBasicAuth = useBasicAuth;
+    }
+    
+    // --
+
+    @XmlElement(name="restfulAuthUser")
+    private String restfulAuthUser;
+    
+    public String getRestfulAuthUser() {
+        return restfulAuthUser;
+    }
+    
+    public void setRestfulAuthUser(String restfulAuthUser) {
+        this.restfulAuthUser = restfulAuthUser;
+    }
+    
+    // --
+
+    @XmlElement(name="restfulAuthPassword")
+    private String restfulAuthPassword;
+    
+    public String getRestfulAuthPassword() {
+        return restfulAuthPassword;
+    }
+    
+    public void setRestfulAuthPassword(String restfulAuthPassword) {
+        this.restfulAuthPassword = restfulAuthPassword;
+    }
+    
+    
+}
diff --git a/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClientException.java b/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClientException.java
new file mode 100644
index 0000000..2aaa99a
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/client/RestfulClientException.java
@@ -0,0 +1,40 @@
+/*
+ *  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.applib.client;
+
+public class RestfulClientException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public RestfulClientException() {
+    }
+
+    public RestfulClientException(final String message) {
+        super(message);
+    }
+
+    public RestfulClientException(final Throwable cause) {
+        super(cause);
+    }
+
+    public RestfulClientException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/core/applib/src/main/java/org/apache/isis/applib/client/SuppressionType.java b/core/applib/src/main/java/org/apache/isis/applib/client/SuppressionType.java
new file mode 100644
index 0000000..78467f1
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/client/SuppressionType.java
@@ -0,0 +1,88 @@
+/*
+ *  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.applib.client;
+
+import static org.apache.isis.commons.internal.base._NullSafe.stream;
+
+import java.util.EnumSet;
+import java.util.List;
+
+import org.apache.isis.commons.internal.base._NullSafe;
+
+/**
+ * 
+ * @since 2.0.0-M2
+ */
+public enum SuppressionType {
+
+    /** suppress '$$RO', RO Spec representation*/
+    RO,
+
+    /** suppress '$$href', hyperlink to the representation*/
+    HREF,
+
+    /** suppress '$$instanceId', instance id of the domain object*/
+    ID,
+
+    /** suppress '$$title', title of the domain object*/
+    TITLE,
+
+    /** suppress all '$$...' entries*/
+    ALL
+    ;
+
+    public static EnumSet<SuppressionType> setOf(SuppressionType ... types){
+        final EnumSet<SuppressionType> set = EnumSet.noneOf(SuppressionType.class);
+        stream(types).forEach(set::add);
+        return set;
+    }
+
+    public static class ParseUtil {
+
+        public static EnumSet<SuppressionType> parse(List<String> parameterList) {
+            final EnumSet<SuppressionType> set = EnumSet.noneOf(SuppressionType.class);
+            parameterList.stream()
+            .map(SuppressionType.ParseUtil::parseOrElseNull)
+            .filter(_NullSafe::isPresent)
+            .forEach(set::add);
+            if(set.contains(ALL)) {
+                return EnumSet.allOf(SuppressionType.class);
+            }
+            return set;
+        }
+
+        private static SuppressionType parseOrElseNull(String literal) {
+
+            // honor pre v2 behavior
+            if("true".equalsIgnoreCase(literal)) {
+                return SuppressionType.RO; 
+            }
+
+            try {
+                return SuppressionType.valueOf(literal.toUpperCase());
+            } catch (IllegalArgumentException  e) {
+                return null;
+            }
+        }
+
+    }
+
+
+
+}
\ No newline at end of file
diff --git a/core/applib/src/main/java/org/apache/isis/applib/client/auth/BasicAuthFilter.java b/core/applib/src/main/java/org/apache/isis/applib/client/auth/BasicAuthFilter.java
new file mode 100644
index 0000000..3f4f39e
--- /dev/null
+++ b/core/applib/src/main/java/org/apache/isis/applib/client/auth/BasicAuthFilter.java
@@ -0,0 +1,95 @@
+/*
+ *  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.applib.client.auth;
+
+import static org.apache.isis.commons.internal.base._With.requires;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.xml.bind.DatatypeConverter;
+
+import org.apache.isis.commons.internal.base._Strings;
+
+/**
+ * 
+ * @since 2.0.0-M2
+ */
+public class BasicAuthFilter implements ClientRequestFilter {
+
+    /**
+     * 
+     * @since 2.0.0-M2
+     */
+    public static class Credentials {
+        final String user;
+        final String pass;
+        public static Credentials empty() {
+            return new Credentials("anonymous", null);
+        }
+        public static Credentials of(String user, String pass) {
+            if(_Strings.isNullOrEmpty(user)) {
+                return empty();
+            }
+            return new Credentials(user, pass);
+        }
+        private Credentials(String user, String pass) {
+            this.user = user;
+            this.pass = pass;
+        }
+        @Override
+        public String toString() {
+            return "" + user + ":" + pass;
+        }
+    }
+    
+    public static BasicAuthFilter of(Credentials credentials) {
+        BasicAuthFilter filter = new BasicAuthFilter();
+        filter.setCredentials(credentials);
+        return filter;
+    }
+    
+    private Credentials credentials = Credentials.empty();
+    
+    public Credentials getCredentials() {
+        return credentials;
+    }
+
+    public void setCredentials(Credentials credentials) {
+        this.credentials = requires(credentials, "credentials");
+    }
+
+    @Override
+    public void filter(ClientRequestContext requestContext) throws IOException {
+        requestContext.getHeaders().add("Authorization", getAuthorizationValue());
+    }
+
+    // -- HELPER
+    
+    private String getAuthorizationValue() {
+        try {
+            return "Basic " + DatatypeConverter.printBase64Binary(credentials.toString().getBytes("UTF-8"));
+        } catch (UnsupportedEncodingException ex) {
+            throw new IllegalStateException("Cannot encode with UTF-8", ex);
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/core/commons/src/main/java/org/apache/isis/commons/internal/base/_Strings.java b/core/commons/src/main/java/org/apache/isis/commons/internal/base/_Strings.java
index d75b615..38fd902 100644
--- a/core/commons/src/main/java/org/apache/isis/commons/internal/base/_Strings.java
+++ b/core/commons/src/main/java/org/apache/isis/commons/internal/base/_Strings.java
@@ -24,9 +24,11 @@ import static org.apache.isis.commons.internal.base._Strings_SplitIterator.split
 import static org.apache.isis.commons.internal.base._With.mapIfPresentElse;
 import static org.apache.isis.commons.internal.base._With.requires;
 
+import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.util.Arrays;
 import java.util.Map;
+import java.util.Scanner;
 import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.function.UnaryOperator;
@@ -332,6 +334,20 @@ public final class _Strings {
         requires(replacement, "replacement");
         return mapIfPresentElse(input, __->input.replaceAll("\\s+", replacement), null);
     }
+    
+    // -- READ FROM INPUT STREAM
+    
+    public static String read(@Nullable final InputStream input, Charset charset) {
+        requires(charset, "charset");
+        if(input==null) {
+            return "";
+        }
+        // see https://stackoverflow.com/questions/309424/how-to-read-convert-an-inputstream-into-a-string-in-java
+        try(Scanner scanner = new Scanner(input, charset.name())){
+            scanner.useDelimiter("\\A");
+            return scanner.hasNext() ? scanner.next() : "";
+        }
+    }
 
     // -- BYTE ARRAY CONVERSION
 
@@ -424,5 +440,7 @@ public final class _Strings {
         return suffix(fileName, prefix(fileExtension, "."));
     }
 
+    
+
 
 }
diff --git a/core/viewer-restfulobjects-rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheIsisV1.java b/core/viewer-restfulobjects-rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheIsisV1.java
index 94ba534..2534fc9 100644
--- a/core/viewer-restfulobjects-rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheIsisV1.java
+++ b/core/viewer-restfulobjects-rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheIsisV1.java
@@ -29,7 +29,7 @@ import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.NatureOfService;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.annotation.Where;
-import org.apache.isis.commons.internal.base._NullSafe;
+import org.apache.isis.applib.client.SuppressionType;
 import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
 import org.apache.isis.core.metamodel.consent.Consent;
 import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
@@ -54,52 +54,6 @@ import org.apache.isis.viewer.restfulobjects.rendering.service.RepresentationSer
         )
 public class ContentNegotiationServiceOrgApacheIsisV1 extends ContentNegotiationServiceAbstract {
     
-    public static enum SuppressionType {
-        
-        /** suppress '$$RO', RO Spec representation*/
-        RO,
-        
-        /** suppress '$$href', hyperlink to the representation*/
-        HREF,
-        
-        /** suppress '$$instanceId', instance id of the domain object*/
-        ID,
-        
-        /** suppress '$$title', title of the domain object*/
-        TITLE,
-        
-        /** suppress all '$$...' entries*/
-        ALL
-        ;
-
-        public static EnumSet<SuppressionType> parse(List<String> parameterList) {
-            final EnumSet<SuppressionType> set = EnumSet.noneOf(SuppressionType.class);
-            parameterList.stream()
-            .map(SuppressionType::parseOrElseNull)
-            .filter(_NullSafe::isPresent)
-            .forEach(set::add);
-            if(set.contains(ALL)) {
-                return EnumSet.allOf(SuppressionType.class);
-            }
-            return set;
-        }
-        
-        private static SuppressionType parseOrElseNull(String literal) {
-            
-            // honor pre v2 behavior
-            if("true".equalsIgnoreCase(literal)) {
-                return SuppressionType.RO; 
-            }
-            
-            try {
-                return SuppressionType.valueOf(literal.toUpperCase());
-            } catch (IllegalArgumentException  e) {
-                return null;
-            }
-        }
-        
-    }
-
     /**
      * Unlike RO v1.0, use a single content-type of <code>application/json;profile="urn:org.apache.isis/v1"</code>.
      *
@@ -344,7 +298,7 @@ public class ContentNegotiationServiceOrgApacheIsisV1 extends ContentNegotiation
     protected EnumSet<SuppressionType> suppress(
             final RepresentationService.Context2 rendererContext) {
         final List<MediaType> acceptableMediaTypes = rendererContext.getAcceptableMediaTypes();
-        return SuppressionType.parse(mediaTypeParameterList(acceptableMediaTypes, "suppress"));
+        return SuppressionType.ParseUtil.parse(mediaTypeParameterList(acceptableMediaTypes, "suppress"));
     }
     
     private void appendObjectTo(