[java-sp-server] branch main updated: Park DDF code here temporarily.
Scott Cantor
cantor.2 at osu.edu
Thu Sep 1 18:15:19 UTC 2022
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-sp-server.
View the commit online:
http://git.shibboleth.net/view/?p=java-sp-server.git;a=commit;h=f58522d53ed3bc93b144379e98b83217e78b50f4
The following commit(s) were added to refs/heads/main by this push:
new f58522d Park DDF code here temporarily.
f58522d is described below
commit f58522d53ed3bc93b144379e98b83217e78b50f4
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Sep 1 14:15:16 2022 -0400
Park DDF code here temporarily.
---
.../net/shibboleth/sp/conf/services-system.xml | 2 +-
sp-server-api/pom.xml | 25 -
.../main/java/net/shibboleth/sp/Application.java | 30 +
.../java/support/ddf/ByteArrayToDDFConverter.java | 41 +
.../shibboleth/utilities/java/support/ddf/DDF.java | 1602 ++++++++++++++++++++
.../utilities/java/support/ddf/DDFSupport.java | 101 ++
.../java/support/ddf/DDFToByteArrayConverter.java | 42 +
.../support/ddf/RemotedHttpServletRequest.java | 688 +++++++++
.../support/ddf/RemotedHttpServletResponse.java | 504 ++++++
.../utilities/java/support/ddf/package-info.java | 28 +
.../utilities/java/support/ddf/DDFSupportTest.java | 96 ++
.../utilities/java/support/ddf/DDFTest.java | 480 ++++++
.../support/ddf/RemotedHttpServletRequestTest.java | 171 +++
.../ddf/RemotedHttpServletResponseTest.java | 143 ++
sp-server-impl/pom.xml | 25 -
.../net/shibboleth/sp/impl/BasicApplication.java | 46 +
16 files changed, 3973 insertions(+), 51 deletions(-)
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/services-system.xml b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/services-system.xml
index e6b31c2..dd8bd86 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/services-system.xml
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/services-system.xml
@@ -28,7 +28,7 @@
-->
<bean id="shibboleth.LoggingService"
- class="%{sp.service.logging.class:net.shibboleth.ext.spring.service.LogbackLoggingService}"
+ class="%{sp.service.logging.class:net.shibboleth.shared.service.impl.LogbackLoggingService}"
p:loggingConfiguration="%{sp.service.logging.resource:%{sp.home}/conf/logback.xml}"
p:fallbackConfiguration="classpath:/logback.xml"
p:reloadCheckDelay="%{sp.service.logging.checkInterval:PT0S}"
diff --git a/sp-server-api/pom.xml b/sp-server-api/pom.xml
index a57c11e..be5dc5f 100644
--- a/sp-server-api/pom.xml
+++ b/sp-server-api/pom.xml
@@ -21,18 +21,6 @@
<dependencies>
<!-- compile dependencies -->
- <dependency>
- <groupId>net.shibboleth.utilities</groupId>
- <artifactId>java-support</artifactId>
- <version>${java-support.version}</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>net.shibboleth.ext</groupId>
- <artifactId>spring-extensions</artifactId>
- <version>${spring-extensions.version}</version>
- <scope>compile</scope>
- </dependency>
<dependency>
<groupId>${spring.groupId}</groupId>
<artifactId>spring-beans</artifactId>
@@ -61,19 +49,6 @@
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>net.shibboleth.ext</groupId>
- <artifactId>spring-extensions</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>net.shibboleth.utilities</groupId>
- <artifactId>java-support</artifactId>
- <version>${java-support.version}</version>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
</dependencies>
</project>
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
index a24b3cc..d924d47 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
@@ -21,6 +21,7 @@ import javax.annotation.concurrent.ThreadSafe;
import net.shibboleth.sp.remoting.Endpoint;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
+import net.shibboleth.utilities.java.support.service.ReloadableService;
/**
* A collection of resources protected by the SP and treated as a unit for the purposes of
@@ -39,4 +40,33 @@ public interface Application extends IdentifiedComponent, Endpoint {
@Nonnull @NotEmpty static String OPERATION = "op";
// TODO: Expose various injected services for use by ApplicationEndpoints.
+
+ /**
+ * Get {@link MetadataResolver} for this {@link Application}.
+ *
+ * @return the metadata source to use
+ */
+ @Nonnull ReloadableService<Object> getMetadataResolver();
+
+ /**
+ * Get {@link AttributeTranscoderRegistry} for this {@link Application}.
+ *
+ * @return the attribute filter to use
+ */
+ @Nonnull ReloadableService<Object> getAttributeTranscoderRegistry();
+
+ /**
+ * Get {@link AttributeResolver} for this {@link Application}.
+ *
+ * @return the attribute resolver to use
+ */
+ @Nonnull ReloadableService<Object> getAttributeResolver();
+
+ /**
+ * Get {@link AttributeFilter} for this {@link Application}.
+ *
+ * @return the attribute filter to use
+ */
+ @Nonnull ReloadableService<Object> getAttributeFilter();
+
}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/ByteArrayToDDFConverter.java b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/ByteArrayToDDFConverter.java
new file mode 100644
index 0000000..22c31e1
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/ByteArrayToDDFConverter.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import org.springframework.core.convert.converter.Converter;
+
+/**
+ * Spring converter from byte array to {@link DDF} object.
+ *
+ * @since 7.0.0
+ */
+public class ByteArrayToDDFConverter implements Converter<byte[], DDF> {
+
+ /** {@inheritDoc} */
+ public DDF convert(final byte[] source) {
+ try (final ByteArrayInputStream bais = new ByteArrayInputStream(source)) {
+ return DDF.deserialize(bais);
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDF.java b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDF.java
new file mode 100644
index 0000000..29273e7
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDF.java
@@ -0,0 +1,1602 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * The core object in the DDF mode, this is a node in a tree of objects that
+ * make up the entire data structure.
+ *
+ * <p>Each node contains references to its parant and children, if any,
+ * as well as data type and possibly a value if a leaf node.</p>
+ *
+ * <p>Most of the types are self-explanatory, but strings may be "safe" or "unsafe".
+ * Safe strings are understood to be Unicode that can be safely converted between UTF-8
+ * and UTF-16. Unsafe strings are represented as Java String objects but have an unknown
+ * character encoding so the individual code points above 127 are essentially undefined
+ * and cannot be assumed to represent the "correct" value. They may only be compared
+ * with other values that are understood to represent the same range of values.</p>
+ *
+ * <p>The method names do not align to normal Java conventions for compatibility with
+ * the other version(s) of the same API.</p>
+ */
+ at NotThreadSafe
+public class DDF implements Iterable<DDF> {
+
+ /** Name of node. */
+ @Nullable private String name;
+
+ /** Parent node. */
+ @Nullable private DDF parent;
+
+ /** Type enum. */
+ public enum DDFType {
+
+ /** A null node. */
+ DDF_NULL(-1),
+
+ /** An empty node with no value. */
+ DDF_EMPTY(0),
+
+ /** A string value. */
+ DDF_STRING(1),
+
+ /** An integral value of no more than 32-bits. */
+ DDF_INT(2),
+
+ /** A floating point value. */
+ DDF_FLOAT(3),
+
+ /** A structure with named children. */
+ DDF_STRUCT(4),
+
+ /** An ordered list. */
+ DDF_LIST(5),
+
+ /** A reference to any object. */
+ DDF_POINTER(6),
+
+ /** A string that cannot be assumed to be UTF-8 (see above docs). */
+ DDF_STRING_UNSAFE(7),
+
+ /** An integral value of no more than 64-bits. */
+ DDF_LONG(8);
+
+ /** Type value. */
+ private final int value;
+
+ /**
+ * Constructor.
+ *
+ * @param val value of the type enum
+ */
+ private DDFType(final int val) {
+ value = val;
+ }
+
+ /**
+ * Get the type value.
+ *
+ * @return type value
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Convert an integer into the corresponding enum value.
+ *
+ * @param val input type
+ *
+ * @return enum constant
+ *
+ * @throws IllegalArgumentException if the type is out of range
+ */
+// Checkstyle: CyclomaticComplexity OFF
+ public static DDFType valueOf(final int val) throws IllegalArgumentException {
+ final DDFType type;
+ switch (val) {
+ case -1:
+ type = DDF_NULL;
+ break;
+
+ case 0:
+ type = DDF_EMPTY;
+ break;
+
+ case 1:
+ type = DDF_STRING;
+ break;
+
+ case 2:
+ type = DDF_INT;
+ break;
+
+ case 3:
+ type = DDF_FLOAT;
+ break;
+
+ case 4:
+ type = DDF_STRUCT;
+ break;
+
+ case 5:
+ type = DDF_LIST;
+ break;
+
+ case 6:
+ type = DDF_POINTER;
+ break;
+
+ case 7:
+ type = DDF_STRING_UNSAFE;
+ break;
+
+ case 8:
+ type = DDF_LONG;
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unrecognized DDF type");
+ }
+ return type;
+ }
+
+ };
+// Checkstyle: CyclomaticComplexity ON
+
+ /** Node type. */
+ @Nonnull private DDFType type;
+
+ /** Reference to the value, which depends on the type. */
+ @Nullable private Object value;
+
+ /** Constructor. */
+ public DDF() {
+ type = DDFType.DDF_NULL;
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * @param n node name
+ */
+ public DDF(@Nullable @NotEmpty final String n) {
+ type = DDFType.DDF_EMPTY;
+ name(n);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * @param n node name
+ * @param val string value, assumed to be "safe" Unicode
+ */
+ public DDF(@Nullable @NotEmpty final String n, @Nullable final String val) {
+ this(n);
+ string(val);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * @param n node name
+ * @param val byte array value, handled without knowledge of the encoding
+ */
+ public DDF(@Nullable @NotEmpty final String n, @Nullable final byte[] val) {
+ this(n);
+ unsafe_string(val);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * @param n node name
+ * @param val integer value
+ */
+ public DDF(@Nullable @NotEmpty final String n, final int val) {
+ this(n);
+ integer(val);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * @param n node name
+ * @param val long integer value
+ */
+ public DDF(@Nullable @NotEmpty final String n, final long val) {
+ this(n);
+ longinteger(val);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * @param n node name
+ * @param val floating value
+ */
+ public DDF(@Nullable @NotEmpty final String n, final double val) {
+ this(n);
+ floating(val);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * @param n node name
+ * @param val object value
+ */
+ public DDF(@Nullable @NotEmpty final String n, @Nullable final Object val) {
+ this(n);
+ pointer(val);
+ }
+
+ /**
+ * Destroys a node's content, resets it to a null object and clears its name.
+ *
+ * <p>This is primarily for tree maintenance, given the lack of need for explicit
+ * memory management.</p>
+ *
+ * @return this object
+ */
+ @Nonnull public DDF destroy() {
+ remove().empty().name(null);
+ type = DDFType.DDF_NULL;
+ return this;
+ }
+
+ /**
+ * Performs a deep copy of the node and all children, if any.
+ *
+ * @return the copy
+ */
+// Checkstyle: CyclomaticComplexity OFF
+ @SuppressWarnings("unchecked")
+ @Nonnull DDF copy() {
+ final DDF dup = new DDF(name);
+
+ switch (type) {
+ case DDF_NULL:
+ dup.destroy();
+ break;
+
+ case DDF_EMPTY:
+ break;
+
+ case DDF_STRING:
+ dup.string((String) value);
+ break;
+
+ case DDF_STRING_UNSAFE:
+ dup.unsafe_string((byte[]) value);
+ break;
+
+ case DDF_INT:
+ dup.integer((Integer) value);
+ break;
+
+ case DDF_LONG:
+ dup.longinteger((Long) value);
+ break;
+
+ case DDF_FLOAT:
+ dup.floating((Double) value);
+ break;
+
+ case DDF_STRUCT:
+ dup.structure();
+ for (final DDF ddf : ((Map<String,DDF>) value).values()) {
+ dup.add(ddf.copy());
+ }
+ break;
+
+ case DDF_LIST:
+ dup.list();
+ for (final DDF ddf : (List<DDF>) value) {
+ dup.add(ddf.copy());
+ }
+ break;
+
+ default:
+ }
+
+ return dup;
+ }
+// Checkstyle: CyclomaticComplexity ON
+
+ /**
+ * Get the node name.
+ *
+ * @return the name
+ */
+ @Nullable public String name() {
+ return name;
+ }
+
+ /**
+ * Set the node name.
+ *
+ * <p>For compatibility, the name is constrained to no more than 255 characters.</p>
+ *
+ * <p>The name will not be set if the node is already a child of a structure.</p>
+ *
+ * @param n the new name
+ *
+ * @return this object
+ */
+ @Nonnull public DDF name(@Nullable @NotEmpty final String n) {
+ if (!isnull() && (parent == null || !parent.isstruct())) {
+ if (n != null) {
+ name = Constraint.isNotEmpty(n.substring(0,Integer.min(n.length(), 255)), "Name cannot be empty");
+ } else {
+ name = null;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Returns true iff the node is null.
+ *
+ * @return true iff the node is null
+ */
+ public boolean isnull() {
+ return type == DDFType.DDF_NULL;
+ }
+
+ /**
+ * Returns true iff the node is empty.
+ *
+ * @return true iff the node is empty
+ */
+ public boolean isempty() {
+ return type == DDFType.DDF_EMPTY;
+ }
+
+ /**
+ * Returns true iff the node is a string.
+ *
+ * @return true iff the node is a string
+ */
+ public boolean isstring() {
+ return type == DDFType.DDF_STRING;
+ }
+
+ /**
+ * Returns true iff the node is an unsafe string.
+ *
+ * @return true iff the node is an unsafe string
+ */
+ public boolean isunsafestring() {
+ return type == DDFType.DDF_STRING_UNSAFE;
+ }
+
+ /**
+ * Returns true iff the node is an integer.
+ *
+ * @return true iff the node is an integer
+ */
+ public boolean isint() {
+ return type == DDFType.DDF_INT;
+ }
+
+ /**
+ * Returns true iff the node is a long integer.
+ *
+ * @return true iff the node is a long integer
+ */
+ public boolean islong() {
+ return type == DDFType.DDF_LONG;
+ }
+
+ /**
+ * Returns true iff the node is a floating point.
+ *
+ * @return true iff the node is a floating point
+ */
+ public boolean isfloat() {
+ return type == DDFType.DDF_FLOAT;
+ }
+
+ /**
+ * Returns true iff the node is a structure.
+ *
+ * @return true iff the node is a structure
+ */
+ public boolean isstruct() {
+ return type == DDFType.DDF_STRUCT;
+ }
+
+ /**
+ * Returns true iff the node is a list/array.
+ *
+ * @return true iff the node is a list/array.
+ */
+ public boolean islist() {
+ return type == DDFType.DDF_LIST;
+ }
+
+ /**
+ * Returns true iff the node is a pointer (i.e., object reference).
+ *
+ * @return true iff the node is a pointer (i.e., object reference)
+ */
+ public boolean ispointer() {
+ return type == DDFType.DDF_POINTER;
+ }
+
+ /**
+ * Get the string value of this node.
+ *
+ * <p>The string value of a non-string value is null.</p>
+ *
+ * @return the string value or null
+ */
+ @Nullable public String string() {
+ return isstring() ? (String) value : null;
+ }
+
+ /**
+ * Get the byte array value of this node if an unsafe string.
+ *
+ * @return the byte array value or null
+ */
+// Checkstyle: MethodName OFF
+ @Nullable public byte[] unsafe_string() {
+ return isunsafestring() ? (byte[]) value : null;
+ }
+// Checkstyle: MethodName ON
+
+ /**
+ * Get the integer value of this node.
+ *
+ * <p>Integers are coerced from other types based on numeric conversions
+ * or the count of a structure or list.</p>
+ *
+ * @return the integer value or null
+ */
+ @Nullable public Integer integer() {
+
+ switch(type) {
+ case DDF_INT:
+ return (Integer) value;
+ case DDF_LONG:
+ return ((Long) value).intValue();
+ case DDF_FLOAT:
+ return ((Double) value).intValue();
+ case DDF_STRING:
+ try {
+ return Integer.valueOf((String) value);
+ } catch (final NumberFormatException e) {
+ // Swallow.
+ return null;
+ }
+ case DDF_STRUCT:
+ return ((Map<?,?>) value).size();
+ case DDF_LIST:
+ return ((List<?>) value).size();
+ default:
+ break;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the long integer value of this node.
+ *
+ * <p>Longs are coerced from other types based on numeric conversions
+ * or the count of a structure or list.</p>
+ *
+ * @return the long integer value or null
+ */
+ @Nullable public Long longinteger() {
+
+ switch(type) {
+ case DDF_INT:
+ return ((Integer) value).longValue();
+ case DDF_LONG:
+ return (Long) value;
+ case DDF_FLOAT:
+ return ((Double) value).longValue();
+ case DDF_STRING:
+ try {
+ return Long.valueOf((String) value);
+ } catch (final NumberFormatException e) {
+ // Swallow.
+ return null;
+ }
+ case DDF_STRUCT:
+ return (long) ((Map<?,?>) value).size();
+ case DDF_LIST:
+ return (long) ((List<?>) value).size();
+ default:
+ break;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the floating point value of this node.
+ *
+ * <p>Doubles are coerced from other types based on numeric conversions
+ * or the count of a structure or list.</p>
+ *
+ * @return the floating point value or null
+ */
+ @Nullable public Double floating() {
+
+ switch(type) {
+ case DDF_INT:
+ return ((Integer) value).doubleValue();
+ case DDF_LONG:
+ return ((Long) value).doubleValue();
+ case DDF_FLOAT:
+ return (Double) value;
+ case DDF_STRING:
+ try {
+ return Double.valueOf((String) value);
+ } catch (final NumberFormatException e) {
+ // Swallow.
+ return null;
+ }
+ case DDF_STRUCT:
+ return (double) ((Map<?,?>) value).size();
+ case DDF_LIST:
+ return (double) ((List<?>) value).size();
+ default:
+ break;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the pointer/reference value of this node, which is just an {@link Object}.
+ *
+ * @return pointer/reference value or null
+ */
+ @Nullable public Object pointer() {
+ return ispointer() ? value : null;
+ }
+
+
+ /**
+ * Converts this node to an empty type/value.
+ *
+ * <p>All children should be considered disposed of, though in Java this is
+ * circumventable by means of maintaining references to them.</p>
+ *
+ * @return this object
+ */
+ @Nonnull public DDF empty() {
+ type = DDFType.DDF_EMPTY;
+ value = null;
+ return this;
+ }
+
+ /**
+ * Converts this node to a string type/value.
+ *
+ * @param val the value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF string(@Nullable final String val) {
+ empty();
+ value = val;
+ type = DDFType.DDF_STRING;
+ return this;
+ }
+
+ /**
+ * Converts this node to an unsafe string type/value.
+ *
+ * @param val the value to inject
+ *
+ * @return this object
+ */
+// Checkstyle: MethodName OFF
+ @Nonnull public DDF unsafe_string(@Nullable final byte[] val) {
+ empty();
+ value = val;
+ type = DDFType.DDF_STRING_UNSAFE;
+ return this;
+ }
+// Checkstyle: MethodName ON
+
+ /**
+ * Converts this node to a string type/value based on the converted form of the input.
+ *
+ * @param val input value
+ *
+ * @return this object
+ */
+ @Nonnull public DDF string(final int val) {
+ return string(Integer.toString(val));
+ }
+
+ /**
+ * Converts this node to a string type/value based on the converted form of the input.
+ *
+ * @param val input value
+ *
+ * @return this object
+ */
+ @Nonnull public DDF string(final long val) {
+ return string(Long.toString(val));
+ }
+
+ /**
+ * Converts this node to a string type/value based on the converted form of the input.
+ *
+ * @param val input value
+ *
+ * @return this object
+ */
+ @Nonnull public DDF string(final double val) {
+ return string(Double.toString(val));
+ }
+
+ /**
+ * Converts this node to an integer type/value.
+ *
+ * @param val value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF integer(final int val) {
+ empty();
+ value = Integer.valueOf(val);
+ type = DDFType.DDF_INT;
+ return this;
+ }
+
+ /**
+ * Converts this node to an integer type/value based on the converted form of the input.
+ *
+ * <p>A conversion error will assign zero as the value.</p>
+ *
+ * @param val value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF integer(@Nonnull @NotEmpty final String val) {
+ empty();
+ try {
+ return integer(Integer.valueOf(val));
+ } catch (final NumberFormatException e) {
+ return integer(0);
+ }
+ }
+
+ /**
+ * Converts this node to a long integer type/value.
+ *
+ * @param val value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF longinteger(final long val) {
+ empty();
+ value = Long.valueOf(val);
+ type = DDFType.DDF_LONG;
+ return this;
+ }
+
+ /**
+ * Converts this node to a long integer type/value based on the converted form of the input.
+ *
+ * <p>A conversion error will assign zero as the value.</p>
+ *
+ * @param val value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF longinteger(@Nonnull @NotEmpty final String val) {
+ empty();
+ try {
+ return longinteger(Long.valueOf(val));
+ } catch (final NumberFormatException e) {
+ return longinteger(0);
+ }
+ }
+
+ /**
+ * Converts this node to an floating point type/value.
+ *
+ * @param val value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF floating(final double val) {
+ empty();
+ value = Double.valueOf(val);
+ type = DDFType.DDF_FLOAT;
+ return this;
+ }
+
+ /**
+ * Converts this node to a floating point type/value based on the converted form of the input.
+ *
+ * <p>A conversion error will assign zero as the value.</p>
+ *
+ * @param val value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF floating(@Nonnull @NotEmpty final String val) {
+ empty();
+ try {
+ return floating(Double.valueOf(val));
+ } catch (final NumberFormatException e) {
+ return floating(0.0);
+ }
+ }
+
+ /**
+ * Converts this node to a structure.
+ *
+ * @return this object
+ */
+ @Nonnull public DDF structure() {
+ empty();
+ value = new LinkedHashMap<String,DDF>();
+ type = DDFType.DDF_STRUCT;
+ return this;
+ }
+
+ /**
+ * Converts this node to a list/array.
+ *
+ * @return this object
+ */
+ @Nonnull public DDF list() {
+ empty();
+ value = new ArrayList<DDF>();
+ type = DDFType.DDF_LIST;
+ return this;
+ }
+
+ /**
+ * Converts this node to a pointer/reference type.
+ *
+ * @param val value to inject
+ *
+ * @return this object
+ */
+ @Nonnull public DDF pointer(@Nonnull final Object val) {
+ empty();
+ value = val;
+ type = DDFType.DDF_POINTER;
+ return this;
+ }
+
+ /**
+ * Adds a node to the end of a struct or list and returns it.
+ *
+ * <p>If this node is not a struct or list or the child is a null node, then it is returned
+ * with no further action.</p>
+ *
+ * <p>If this node is a struct with an existing member by the same name, the input
+ * replaces that member.</p>
+ *
+ * @param child the child to add
+ *
+ * @return the child
+ */
+ @SuppressWarnings("unchecked")
+ @Nonnull public DDF add(@Nonnull final DDF child) {
+ if ((!isstruct() && !islist()) || child.isnull() || this == child.parent) {
+ return child;
+ }
+
+ if (isstruct()) {
+ if (child.name == null) {
+ return child;
+ }
+ getmember(child.name).destroy();
+ child.remove();
+ ((Map<String,DDF>) value).put(child.name, child);
+ } else {
+ child.remove();
+ ((List<DDF>) value).add(child);
+ }
+
+ child.parent = this;
+ return child;
+ }
+
+ /**
+ * Adds a node to a list prior to a specified node.
+ *
+ * <p>If this node is not a list, does not contain the second parameter, or
+ * either parameter is a null node, then the first parameter is returned with
+ * no further action.</p>
+ *
+ * @param child the child to add
+ * @param before the node to insert the child before
+ *
+ * @return the child
+ */
+ @Nonnull public DDF addbefore(@Nonnull final DDF child, @Nonnull final DDF before) {
+ if (!islist() || child.isnull() || before.parent != this) {
+ return child;
+ }
+
+ child.remove();
+ @SuppressWarnings("unchecked")
+ final List<DDF> list = (List<DDF>) value;
+ list.add(list.indexOf(before), child);
+ child.parent = this;
+ return child;
+ }
+
+ /**
+ * Adds a node to a list after a specified node.
+ *
+ * <p>If this node is not a list, does not contain the second parameter, or
+ * either parameter is a null node, then the first parameter is returned with
+ * no further action.</p>
+ *
+ * @param child the child to add
+ * @param after the node to insert the child after
+ *
+ * @return the child
+ */
+ @Nonnull public DDF addafter(@Nonnull final DDF child, @Nonnull final DDF after) {
+ if (!islist() || child.isnull() || after.parent != this) {
+ return child;
+ }
+
+ child.remove();
+
+ @SuppressWarnings("unchecked")
+ final List<DDF> list = (List<DDF>) value;
+
+ final int i = list.indexOf(after);
+ if (i == list.size() - 1) {
+ list.add(child);
+ } else {
+ list.add(i + 1, child);
+ }
+ child.parent = this;
+
+ return child;
+ }
+
+ /**
+ * Isolate this object from its surrounding nodes and return it.
+ *
+ * @return this object
+ */
+ @SuppressWarnings("unchecked")
+ @Nonnull public DDF remove() {
+ if (parent != null) {
+ if (parent.isstruct()) {
+ ((Map<String,DDF>) parent.value).remove(name);
+ } else {
+ ((List<DDF>) parent.value).remove(this);
+ }
+
+ parent = null;
+ }
+ return this;
+ }
+
+ /**
+ * Get the parent node.
+ *
+ * @return parent node
+ */
+ @Nullable public DDF parent() {
+ return parent;
+ }
+
+ /**
+ * Expose an immutable map representing a structure node.
+ *
+ * @return immutable map, or null if the node is not a structure
+ */
+ @SuppressWarnings("unchecked")
+ @Nonnull @NonnullElements @Unmodifiable @NotLive public Map<String,DDF> asMap() {
+ if (isstruct()) {
+ return Map.copyOf((Map<String,DDF>) value);
+ }
+
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Expose an immutable list representing a structure or list node.
+ *
+ * @return immutable list, or null if the node is not a structure or list
+ */
+ @SuppressWarnings("unchecked")
+ @Nonnull @NonnullElements @Unmodifiable @NotLive public List<DDF> asList() {
+ if (isstruct()) {
+ return List.copyOf(((Map<String,DDF>) value).values());
+ } else if (islist()) {
+ return List.copyOf((List<DDF>) value);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Adds a new empty node to a structure, possibly creating nested structures based
+ * on dotted path notation (existing nodes matching the path segments are not altered
+ * other than to convert them to structures).
+ *
+ * <p>The input path MUST contain at least one non-empty path segment.</p>
+ *
+ * <p>This node will be converted to a structure if not already one.</p>
+ *
+ * @param path dotted path to use
+ *
+ * @return the last node added to the nested tree, or a null node if unable to do so
+ */
+ @Nonnull public DDF addmember(@Nonnull @NotEmpty final String path) {
+ final String[] tokens = Constraint.isNotEmpty(path, "Path cannot be null").split("\\.");
+ Constraint.isNotEmpty(tokens, "Path did not produce an array of path segments");
+
+ if (!isnull()) {
+ DDF base = this;
+ for (final String segment : tokens) {
+ if (!base.isstruct()) {
+ base.structure();
+ }
+
+ DDF node = base.getmember(segment);
+ if (node.isnull()) {
+ node = base.add(new DDF(segment));
+ }
+
+ base = node;
+ }
+
+ return base;
+ }
+
+ return new DDF();
+ }
+
+ /**
+ * Access a (possibly nested) structure member via dotted path notation, also allowing access to
+ * list elements via "[n]" array notation.
+ *
+ * <p>Failure to navigate the tree at any point will cause a null node to be returned.</p>
+ *
+ * @param path dotted path to use
+ *
+ * @return the matching node, or a null node
+ */
+// Checkstyle: CyclomaticComplexity OFF
+ @SuppressWarnings("unchecked")
+ @Nonnull public DDF getmember(@Nonnull @NotEmpty final String path) {
+ final String[] tokens = path.split("\\.");
+ if (tokens == null || tokens.length == 0 || isnull()) {
+ return new DDF();
+ }
+
+ DDF current = this;
+
+ for (int i = 0; i < tokens.length;) {
+ if (tokens[i].startsWith("[") && tokens[i].endsWith("]")) {
+ // Attempt to access a list entry via [n] notation and advance the path.
+ int index;
+ try {
+ index = Integer.valueOf(tokens[i].substring(1, tokens[i].length() - 1));
+ } catch(final NumberFormatException e) {
+ index = 0;
+ }
+ if (current.islist() && index < ((List<DDF>) current.value).size()) {
+ current = ((List<DDF>) current.value).get(index);
+ } else {
+ return new DDF();
+ }
+ i++;
+ } else if (current.isstruct()) {
+ // Access the named element and advance the path.
+ current = ((Map<String,DDF>) current.value).get(tokens[i]);
+ if (current == null) {
+ return new DDF();
+ }
+ i++;
+ } else if (current.islist()) {
+ // Access first element of list, don't advance the path.
+ current = ((List<DDF>) current.value).get(0);
+ if (current == null) {
+ return new DDF();
+ }
+ } else {
+ return new DDF();
+ }
+ }
+
+ return current;
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull public Iterator<DDF> iterator() {
+ final List<DDF> list = asList();
+ if (list != null) {
+ return list.iterator();
+ }
+ return Collections.emptyListIterator();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj == null) {
+ return false;
+ }
+
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final DDF other = (DDF) obj;
+ if (name == null) {
+ if (other.name != null) {
+ return false;
+ }
+ } else if (!name.equals(other.name)) {
+ return false;
+ }
+
+ if (type != other.type) {
+ return false;
+ }
+
+ if (value == null) {
+ if (other.value != null) {
+ return false;
+ }
+ } else if (!value.equals(other.value)) {
+ return false;
+ }
+
+ return true;
+ }
+// Checkstyle: CyclomaticComplexity ON
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((parent == null) ? 0 : parent.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
+ result = prime * result + ((value == null) ? 0 : value.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>The string output is for debugging purposes and should not be used when serializing.</p>
+ */
+ @Override
+ @Nonnull public String toString() {
+ return dump(new StringBuilder(), 0).toString();
+ }
+
+// Checkstyle: MethodLength|CyclomaticComplexity OFF
+ /**
+ * Helper method to dump to a string for debugging.
+ *
+ * @param builder string builder to use
+ * @param indent size of indent
+ *
+ * @return the first parameter
+ */
+ @Nonnull private StringBuilder dump(@Nonnull final StringBuilder builder, final long indent) {
+
+ for (long i = 0; i < indent; ++i) {
+ builder.append(' ');
+ }
+
+ switch (type) {
+
+ case DDF_NULL:
+ builder.append("null");
+ break;
+
+ case DDF_EMPTY:
+ builder.append("empty");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ break;
+
+ case DDF_STRING:
+ builder.append("String");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = ");
+ if (value != null) {
+ builder.append('"').append(((String) value).replace("\"", "\\\"")).append('"');
+ } else {
+ builder.append("null");
+ }
+ break;
+
+ case DDF_STRING_UNSAFE:
+ builder.append("byte[]");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = ");
+ if (value != null) {
+ builder.append('{');
+ for (final byte b : (byte[]) value) {
+ builder.append(Integer.toHexString(b)).append(", ");
+ }
+ builder.append('}');
+ } else {
+ builder.append("null");
+ }
+ break;
+
+ case DDF_INT:
+ builder.append("Integer");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = ").append(value);
+ break;
+
+ case DDF_LONG:
+ builder.append("Long");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = ").append(value);
+ break;
+
+ case DDF_FLOAT:
+ builder.append("Double");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = ").append(value);
+ break;
+
+ case DDF_STRUCT:
+ builder.append("struct");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = {");
+ if (!((Map<?,?>) value).isEmpty()) {
+ builder.append('\n');
+ for (final DDF child : this) {
+ child.dump(builder, indent + 2);
+ }
+ }
+ for (long i = 0; i < indent; ++i) {
+ builder.append(' ');
+ }
+ builder.append('}');
+ break;
+
+ case DDF_LIST:
+ builder.append("DDF[").append(((List<?>) value).size()).append(']');
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = {");
+
+ if (!((List<?>) value).isEmpty()) {
+ builder.append('\n');
+ for (final DDF child : this) {
+ child.dump(builder, indent + 2);
+ }
+ }
+ for (long i = 0; i < indent; ++i) {
+ builder.append(' ');
+ }
+ builder.append('}');
+ break;
+
+ case DDF_POINTER:
+ builder.append("Object");
+ if (name != null) {
+ builder.append(' ').append(name);
+ }
+ builder.append(" = ");
+ if (value != null) {
+ builder.append(value);
+ } else {
+ builder.append("null");
+ }
+ break;
+
+ default:
+ builder.append("UNKNOWN -- WARNING: ILLEGAL VALUE");
+ }
+ builder.append(";\n");
+
+ return builder;
+ }
+
+ /**
+ * Serialize this object to a provided stream.
+ *
+ * @param os output stream
+ *
+ * @return the output stream
+ *
+ * @throws IOException if an error occurs
+ */
+ @Nonnull public OutputStream serialize(@Nonnull final OutputStream os) throws IOException {
+ if (!isnull()) {
+ if (name != null) {
+ encode(os, name.getBytes("UTF8"));
+ } else {
+ os.write('.');
+ }
+ os.write(' ');
+
+ switch (type) {
+ case DDF_EMPTY:
+ case DDF_POINTER:
+ os.write(Integer.toString(DDFType.DDF_EMPTY.getValue()).getBytes("UTF8"));
+ os.write('\n');
+ break;
+
+ case DDF_STRING:
+ os.write(Integer.toString(type.getValue()).getBytes("UTF8"));
+ if (value != null) {
+ os.write(' ');
+ encode(os, ((String) value).getBytes("UTF-8"));
+ }
+ os.write('\n');
+ break;
+
+ case DDF_STRING_UNSAFE:
+ os.write(Integer.toString(type.getValue()).getBytes("UTF8"));
+ if (value != null) {
+ os.write(' ');
+ encode(os, (byte[]) value);
+ }
+ os.write('\n');
+ break;
+
+ case DDF_INT:
+ os.write(Integer.toString(type.getValue()).getBytes("UTF8"));
+ os.write(' ');
+ os.write(Integer.toString((Integer) value).getBytes("UTF8"));
+ os.write('\n');
+ break;
+
+ case DDF_LONG:
+ os.write(Integer.toString(type.getValue()).getBytes("UTF8"));
+ os.write(' ');
+ os.write(Long.toString((Long) value).getBytes("UTF8"));
+ os.write('\n');
+ break;
+
+ case DDF_FLOAT:
+ os.write(Integer.toString(type.getValue()).getBytes("UTF8"));
+ os.write(' ');
+ os.write(Double.toString((Double) value).getBytes("UTF8"));
+ os.write('\n');
+ break;
+
+ case DDF_STRUCT:
+ @SuppressWarnings("unchecked")
+ final Collection<DDF> members = ((Map<String,DDF>) value).values();
+ os.write(Integer.toString(type.getValue()).getBytes("UTF8"));
+ os.write(' ');
+ os.write(Integer.toString(members.size()).getBytes("UTF8"));
+ os.write('\n');
+ for (final DDF child : members) {
+ child.serialize(os);
+ }
+ break;
+
+ case DDF_LIST:
+ @SuppressWarnings("unchecked")
+ final Collection<DDF> children = (List<DDF>) value;
+ os.write(Integer.toString(type.getValue()).getBytes("UTF8"));
+ os.write(' ');
+ os.write(Integer.toString(children.size()).getBytes("UTF8"));
+ os.write('\n');
+ for (final DDF child : children) {
+ child.serialize(os);
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return os;
+ }
+
+ /**
+ * Parses a seralized DDF from an input stream.
+ *
+ * @param is input stream
+ *
+ * @return the parsed object
+ *
+ * @throws IOException if an error occurs
+ */
+// Checkstyle: ReturnCount OFF
+ @Nonnull public static DDF deserialize(@Nonnull final InputStream is) throws IOException {
+
+ int ch;
+ final StringBuilder nameBuilder = new StringBuilder();
+
+ // First field is the name.
+ while ((ch = is.read()) != -1 && !Character.isWhitespace(ch)) {
+ if (ch >= 0 && ch <= 127) {
+ // The int is a code point from 0..255, but our grammar constrains this to 0..127 so
+ // this is a safe append, to promote the ASCII into Unicode.
+ nameBuilder.appendCodePoint(ch);
+ } else {
+ throw new IOException("Invalid code point outside US-ASCII range");
+ }
+ }
+
+ if (ch != 0x20) {
+ // Name has to be followed by a space.
+ // This will also cover an early line or stream termination.
+ throw new IOException("Name not followed by space character");
+ }
+
+ final String name = nameBuilder.toString();
+ if (name.isEmpty()) {
+ // No name field.
+ throw new IOException("Name field missing");
+ }
+
+ final DDF obj = new DDF(null);
+ if (!".".equals(name)) {
+ // The name is stipulated to be UTF-8 safe so any high order ASCII characters are
+ // assumed to be part of a multi-byte sequence.
+ try {
+ obj.name(URLDecoder.decode(name, "UTF-8"));
+ } catch (final IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ // Next field is the numeric type designation.
+ final StringBuilder typeBuilder = new StringBuilder();
+ while ((ch = is.read()) != -1 && Character.isDigit(ch)) {
+ // This is safe because the byte contract of the stream disallows
+ // any non-ASCII digit from satisfying the isDigit check.
+ typeBuilder.appendCodePoint(ch);
+ }
+
+ // Before continuing, we convert the string into a DDF type.
+ final DDFType type;
+ try {
+ type = DDFType.valueOf(Integer.valueOf(typeBuilder.toString()));
+ } catch (final IllegalArgumentException e) {
+ throw new IOException("Invalid DDF type");
+ }
+
+ // Process typical value types.
+ final StringBuilder valueBuilder = new StringBuilder();
+ switch (type) {
+ case DDF_EMPTY:
+ case DDF_POINTER:
+ if (ch != 0x0A) {
+ throw new IOException("Empty/pointer record not terminated by linefeed");
+ }
+ // Nothing else to do, it's already empty.
+ return obj;
+
+ case DDF_STRING:
+ case DDF_STRING_UNSAFE:
+ if (ch == 0x0A) {
+ if (type == DDFType.DDF_STRING) {
+ return obj.string(null);
+ }
+ return obj.unsafe_string(null);
+ } else if (ch != 0x20) {
+ throw new IOException("Type field not followed by space character");
+ }
+
+ while ((ch = is.read()) != -1 && !Character.isWhitespace(ch)) {
+ if (ch >= 0 && ch <= 127) {
+ // The int is a code point from 0..255, but our grammar constrains this to 0..127 so
+ // this is a safe append, to promote the ASCII into Unicode.
+ valueBuilder.appendCodePoint(ch);
+ } else {
+ throw new IOException("Invalid code point outside US-ASCII range");
+ }
+ }
+
+ if (ch != 0x0A) {
+ throw new IOException("String value not followed by linefeed");
+ }
+
+ try {
+ if (type == DDFType.DDF_STRING) {
+ // String values are handled as UTF-8.
+ return obj.string(URLDecoder.decode(valueBuilder.toString(), "UTF-8"));
+ }
+
+ // Unsafe string values are processed as ISO-8859-1.
+ // They may be anything, but it will guarantee a single byte encoding.
+ return obj.unsafe_string(
+ URLDecoder.decode(valueBuilder.toString(), "ISO-8859-1").getBytes("ISO-8859-1"));
+
+ } catch (final IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+
+ case DDF_INT:
+ case DDF_LONG:
+ case DDF_FLOAT:
+ if (ch != 0x20) {
+ throw new IOException("Type field not followed by space character");
+ }
+
+ while ((ch = is.read()) != -1 && !Character.isWhitespace(ch)) {
+ if (ch >= 0 && ch <= 127) {
+ // The int is a code point from 0..255, but our grammar constrains this to 0..127 so
+ // this is a safe append, to promote the ASCII into Unicode.
+ valueBuilder.appendCodePoint(ch);
+ } else {
+ throw new IOException("Invalid code point outside US-ASCII range");
+ }
+ }
+
+ if (ch != 0x0A) {
+ throw new IOException("Numeric value not followed by linefeed");
+ } else if (valueBuilder.length() == 0) {
+ throw new IOException("Numeric value missing");
+ }
+
+ if (type == DDFType.DDF_INT) {
+ return obj.integer(valueBuilder.toString());
+ } else if (type == DDFType.DDF_LONG) {
+ return obj.longinteger(valueBuilder.toString());
+ }
+ return obj.floating(valueBuilder.toString());
+
+ case DDF_STRUCT:
+ case DDF_LIST:
+ if (ch != 0x20) {
+ throw new IOException("Type field not followed by space character");
+ }
+
+ while ((ch = is.read()) != -1 && Character.isDigit(ch)) {
+ // This is safe because the byte contract of the stream disallows
+ // any non-ASCII digit from satisfying the isDigit check.
+ valueBuilder.appendCodePoint(ch);
+ }
+
+ if (ch != 0x0A) {
+ throw new IOException("Record count not followed by linefeed");
+ } else if (valueBuilder.length() == 0) {
+ throw new IOException("Record count missing");
+ }
+
+ int count;
+ try {
+ count = Integer.valueOf(valueBuilder.toString());
+ } catch (final NumberFormatException e) {
+ throw new IOException("Invalid record count");
+ }
+
+ if (type == DDFType.DDF_STRUCT) {
+ obj.structure();
+ } else {
+ obj.list();
+ }
+
+ for (; count > 0; --count) {
+ obj.add(deserialize(is));
+ }
+ return obj;
+
+ default:
+ throw new IOException("Unexpected record type");
+ }
+ }
+// Checkstyle: MethodLength|CyclomaticComplexity|ReturnCount ON
+
+ /**
+ * A simple encoder for non-ASCII characters.
+ *
+ * <p>Made this package-accessible for unit testing.</p>
+ *
+ * @param os output stream
+ * @param bytes bytes to encode
+ *
+ * @throws IOException if an error occurs
+ */
+ static void encode(@Nonnull final OutputStream os, @Nonnull final byte[] bytes) throws IOException {
+ for (final byte b : bytes) {
+ final int i = Byte.toUnsignedInt(b);
+ if (i < 0x30 || i > 0x7A) {
+ os.write('%');
+ os.write(hexchar(i >>> 4));
+ os.write(hexchar(i & 0x0F));
+ } else {
+ os.write(b);
+ }
+ }
+ }
+
+ /**
+ * Converts a byte into a hex character.
+ *
+ * @param b input byte
+ *
+ * @return the hex character equivalent (capitalized)
+ */
+ private static int hexchar(final int b) {
+ // 48 is '0' and 65 is 'A'
+ return (b <= 9) ? (48 + b) : (65 + b - 10);
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDFSupport.java b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDFSupport.java
new file mode 100644
index 0000000..72ddffe
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDFSupport.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import javax.annotation.Nonnull;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.xml.ElementSupport;
+
+/**
+ * Helper methods for {@link DDF} usage.
+ *
+ * @since 9.0.0
+ */
+public final class DDFSupport {
+
+ /** Name of child element member created by {@link #fromElement(Element)} method. */
+ @Nonnull @NotEmpty public static final String CHILD_ELEMENTS_MEMBER = "_children";
+
+ /** Name of content member created by {@link #fromElement(Element)} method. */
+ @Nonnull @NotEmpty public static final String CONTENT_MEMBER = "_content";
+
+ /** Private constructor. */
+ private DDFSupport() {
+
+ }
+
+ /**
+ * Converts a DOM tree rooted at the input element into a {@link DDF} mirroring the tree.
+ *
+ * <p>The name of the object is the local name of the element.
+ *
+ * <p>Attributes are converted into named structure members based on the local names.</p>
+ *
+ * <p>Child elements are recursively processed into a list named {@link #CHILD_ELEMENTS_MEMBER}.</p>
+ *
+ * <p>Text content is stored in a structure member named {@link #CONTENT_MEMBER}.</p>
+ *
+ * <p>Namespaces are ignored.</p>
+ *
+ * @param element input element
+ *
+ * @return the converted object
+ */
+ @Nonnull public static DDF fromElement(@Nonnull final Element element) {
+
+ Constraint.isNotNull(element, "Element cannot be null");
+
+ // Named after element.
+ final DDF obj = new DDF(element.getLocalName());
+
+ // Each attribute is added as a string member.
+ final NamedNodeMap attrs = element.getAttributes();
+ if (attrs != null) {
+ for (int i = 0; i < attrs.getLength(); ++i) {
+ final Attr attr = (Attr) attrs.item(i);
+ obj.addmember(attr.getLocalName()).string(attr.getValue());
+ }
+ }
+
+ DDF children = null;
+
+ // Recursively convert each child to a child DDF and add it to a list.
+ Element child = ElementSupport.getFirstChildElement(element);
+ while (child != null) {
+ if (children == null) {
+ children = obj.addmember(CHILD_ELEMENTS_MEMBER).list();
+ }
+ children.add(fromElement(child));
+ child = ElementSupport.getNextSiblingElement(child);
+ }
+
+ final String content = ElementSupport.getElementContentAsString(element);
+ if (content != null && !content.isBlank()) {
+ obj.addmember(CONTENT_MEMBER).string(content);
+ }
+
+ return obj;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDFToByteArrayConverter.java b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDFToByteArrayConverter.java
new file mode 100644
index 0000000..c2adc0e
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/DDFToByteArrayConverter.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.springframework.core.convert.converter.Converter;
+
+/**
+ * Spring converter from {@link DDF} to byte array.
+ *
+ * @since 7.0.0
+ */
+public class DDFToByteArrayConverter implements Converter<DDF, byte[]> {
+
+ /** {@inheritDoc} */
+ public byte[] convert(final DDF source) {
+ try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ source.serialize(baos);
+ return baos.toByteArray();
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletRequest.java b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletRequest.java
new file mode 100644
index 0000000..bf0a04e
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletRequest.java
@@ -0,0 +1,688 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.security.Principal;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import jakarta.servlet.http.HttpUpgradeHandler;
+import jakarta.servlet.http.Part;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.net.URISupport;
+
+/**
+ * Wraps a {@link DDF} object reflecting a remoted message encapsulating an HTTP request.
+ *
+ * <p>Potential TODOs are noted in various places. One outstanding issue is header case.
+ * The SP code never did case-folding of the header names, so we may need to adjust that
+ * on the C++ side to force-lower them in transit and then lower them on access.</p>
+ *
+ */
+ at NotThreadSafe
+public class RemotedHttpServletRequest implements HttpServletRequest {
+
+ /** Empty byte array for empty bodies. */
+ @Nonnull private static final byte[] EMPTY_BODY = new byte[0];
+
+ /** UTF-8 decoder. */
+ @Nonnull private static final CharsetDecoder UTF_8 =
+ Charset.forName("UTF-8").newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+
+ /** ISO single byte decoder. */
+ @Nonnull private static final CharsetDecoder ISO_8859_1 =
+ Charset.forName("ISO-8859-1").newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+
+ /** Underlying object containing remoted data. */
+ @Nonnull private final DDF obj;
+
+ /** Cookie array. */
+ @Nullable @NonnullElements private List<Cookie> cookies;
+
+ /** Parameter map. */
+ @Nullable private Map<String, String[]> parameters;
+
+ /**
+ * Constructor.
+ *
+ * @param ddf remoted request information
+ */
+ public RemotedHttpServletRequest(@Nonnull final DDF ddf) {
+ obj = Constraint.isNotNull(ddf, "DDF cannot be null");
+ }
+
+ /**
+ * Gets the underlying object containing the remoted data.
+ *
+ * @return remoted data object
+ */
+ @Nonnull public DDF getDDF() {
+ return obj;
+ }
+
+ /** {@inheritDoc} */
+ public Object getAttribute(final String name) {
+ // TODO: might use this to expose certain "standard" pieces of information
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Enumeration<String> getAttributeNames() {
+ // TODO: adjust if we support the method above.
+ return Collections.emptyEnumeration();
+ }
+
+ /** {@inheritDoc} */
+ public String getCharacterEncoding() {
+ return "UTF-8";
+ }
+
+ /** {@inheritDoc} */
+ public void setCharacterEncoding(final String env) throws UnsupportedEncodingException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public int getContentLength() {
+ final Integer i = obj.getmember("content_length").integer();
+ return i != null ? i : -1;
+ }
+
+ /** {@inheritDoc} */
+ public long getContentLengthLong() {
+ return getContentLength();
+ }
+
+ /** {@inheritDoc} */
+ public String getContentType() {
+ return obj.getmember("content_type").string();
+ }
+
+ /** {@inheritDoc} */
+ public ServletInputStream getInputStream() throws IOException {
+ final String body = obj.getmember("body").string();
+ if (body != null) {
+ // The body is always assumed to be safely encoded for our use cases.
+ return new BodyInputStream(body.getBytes("UTF-8"));
+ }
+ return new BodyInputStream(EMPTY_BODY);
+ }
+
+ /** {@inheritDoc} */
+ public String getParameter(final String name) {
+ final String[] values = getParameterMap().get(name);
+ if (values != null) {
+ return values[0];
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Enumeration<String> getParameterNames() {
+ return Collections.enumeration(getParameterMap().keySet());
+ }
+
+ /** {@inheritDoc} */
+ public String[] getParameterValues(final String name) {
+ return getParameterMap().get(name);
+ }
+
+ /** {@inheritDoc} */
+ public Map<String, String[]> getParameterMap() {
+ if (parameters == null) {
+ parameters = new HashMap<>();
+ final Multimap<String,String> multimap = ArrayListMultimap.create();
+ final String qs = getQueryString();
+ if (qs != null) {
+ final List<Pair<String,String>> qparams = URISupport.parseQueryString(qs);
+ for (final Pair<String,String> p : qparams) {
+ multimap.put(p.getFirst(), p.getSecond());
+ }
+ }
+
+ if ("application/x-www-form-urlencoded".equals(getContentType())) {
+ final String body = obj.getmember("body").string();
+ if (body != null) {
+ final List<Pair<String,String>> qparams = URISupport.parseQueryString(body);
+ for (final Pair<String,String> p : qparams) {
+ multimap.put(p.getFirst(), p.getSecond());
+ }
+ }
+ }
+
+ for (final Map.Entry<String,Collection<String>> entry : multimap.asMap().entrySet()) {
+ parameters.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()]));
+ }
+ }
+
+ return parameters;
+ }
+
+ /** {@inheritDoc} */
+ public String getProtocol() {
+ final String protocol = obj.getmember("protocol").string();
+ return protocol != null ? protocol : "HTTP/1.1";
+ }
+
+ /** {@inheritDoc} */
+ public String getScheme() {
+ return obj.getmember("scheme").string();
+ }
+
+ /** {@inheritDoc} */
+ public String getServerName() {
+ return decodeUnsafeString(obj.getmember("hostname").unsafe_string());
+ }
+
+ /** {@inheritDoc} */
+ public int getServerPort() {
+ final Integer i = obj.getmember("port").integer();
+ return i != null ? i : -1;
+ }
+
+ /** {@inheritDoc} */
+ public BufferedReader getReader() throws IOException {
+ return new BufferedReader(new StringReader(obj.getmember("body").string()));
+ }
+
+ /** {@inheritDoc} */
+ public String getRemoteAddr() {
+ return obj.getmember("client_addr").string();
+ }
+
+ /** {@inheritDoc} */
+ public String getRemoteHost() {
+ return obj.getmember("client_addr").string();
+ }
+
+ /** {@inheritDoc} */
+ public void setAttribute(final String name, final Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public void removeAttribute(final String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public Locale getLocale() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public Enumeration<Locale> getLocales() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public boolean isSecure() {
+ return "https".equals(getScheme());
+ }
+
+ /** {@inheritDoc} */
+ public RequestDispatcher getRequestDispatcher(final String path) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public String getRealPath(final String path) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public int getRemotePort() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public String getLocalName() {
+ // TODO: If we need this, should be configurable via the c'tor.
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public String getLocalAddr() {
+ // TODO: If we need this, should be configurable via the c'tor.
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public int getLocalPort() {
+ return getServerPort();
+ }
+
+ /** {@inheritDoc} */
+ public ServletContext getServletContext() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public AsyncContext startAsync() throws IllegalStateException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public AsyncContext startAsync(final ServletRequest servletRequest, final ServletResponse servletResponse)
+ throws IllegalStateException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public boolean isAsyncStarted() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isAsyncSupported() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public AsyncContext getAsyncContext() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public DispatcherType getDispatcherType() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public String getAuthType() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Cookie[] getCookies() {
+ if (cookies == null) {
+ final String header = getHeader("Cookie");
+ if (header != null) {
+ final String[] carray = header.split(";");
+ if (carray != null) {
+ cookies = new ArrayList<>(carray.length);
+ for (final String c : carray) {
+ final String[] nvpair = c.split("=", -1);
+ if (nvpair.length == 2) {
+ final String name = nvpair[0].trim();
+ // This is a fallback cookie used for Safari to work around SameSite bugs.
+ if (name.endsWith("_fgwars")) {
+ name.substring(0, name.length() - 7);
+ }
+ cookies.add(new Cookie(name, nvpair[1]));
+ }
+ }
+ } else {
+ cookies = Collections.emptyList();
+ }
+ } else {
+ cookies = Collections.emptyList();
+ }
+ }
+
+ if (cookies.isEmpty()) {
+ return null;
+ }
+ return cookies.toArray(new Cookie[cookies.size()]);
+ }
+
+ /** {@inheritDoc} */
+ public long getDateHeader(final String name) {
+ final DDF h = obj.getmember("headers").getmember(name);
+ if (h.isstring()) {
+ try {
+ // TODO: there are a ton of valid formats but I don't think we really will need this anyway.
+ final SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
+ formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+ final Date d = formatter.parse(h.string());
+ if (d != null) {
+ return d.getTime();
+ }
+ } catch (final Exception e) {
+ // Ignore for now.
+ }
+ }
+ return -1;
+ }
+
+ /** {@inheritDoc} */
+ public String getHeader(final String name) {
+ return decodeUnsafeString(obj.getmember("headers").getmember(name).unsafe_string());
+ }
+
+ /** {@inheritDoc} */
+ public Enumeration<String> getHeaders(final String name) {
+ final String s = decodeUnsafeString(obj.getmember("headers").getmember(name).unsafe_string());
+ if (s != null) {
+ return Collections.enumeration(Collections.singletonList(s));
+ }
+ return Collections.emptyEnumeration();
+ }
+
+ /** {@inheritDoc} */
+ public Enumeration<String> getHeaderNames() {
+ return Collections.enumeration(obj.getmember("headers").asMap().keySet());
+ }
+
+ /** {@inheritDoc} */
+ public int getIntHeader(final String name) {
+ final DDF h = obj.getmember("headers").getmember(name);
+ if (h.isstring()) {
+ return Integer.parseInt(decodeUnsafeString(h.unsafe_string()));
+ }
+ return -1;
+ }
+
+ /** {@inheritDoc} */
+ public String getMethod() {
+ return obj.getmember("method").string();
+ }
+
+ /** {@inheritDoc} */
+ public String getPathInfo() {
+ // TODO: If we need this, should be configurable via the c'tor.
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public String getPathTranslated() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getContextPath() {
+ // TODO: If we need this, should be configurable via the c'tor.
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public String getQueryString() {
+ return obj.getmember("query").string();
+ }
+
+ /** {@inheritDoc} */
+ public String getRemoteUser() {
+ return obj.getmember("remote_user").string();
+ }
+
+ /** {@inheritDoc} */
+ public boolean isUserInRole(final String role) {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public Principal getUserPrincipal() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getRequestedSessionId() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getRequestURI() {
+ return decodeUnsafeString(obj.getmember("uri").unsafe_string());
+ }
+
+ /** {@inheritDoc} */
+ public StringBuffer getRequestURL() {
+ final String url = decodeUnsafeString(obj.getmember("url").unsafe_string());
+ return new StringBuffer(url != null ? url : "");
+ }
+
+ /** {@inheritDoc} */
+ public String getServletPath() {
+ // TODO: If we need this, should be configurable via the c'tor.
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public HttpSession getSession(final boolean create) {
+ if (create) {
+ throw new UnsupportedOperationException();
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public HttpSession getSession() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String changeSessionId() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public boolean isRequestedSessionIdValid() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isRequestedSessionIdFromCookie() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isRequestedSessionIdFromURL() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isRequestedSessionIdFromUrl() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public boolean authenticate(final HttpServletResponse response) throws IOException, ServletException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public void login(final String username, final String password) throws ServletException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public void logout() throws ServletException {
+
+ }
+
+ /** {@inheritDoc} */
+ public Collection<Part> getParts() throws IOException, ServletException {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Part getPart(final String name) throws IOException, ServletException {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public <T extends HttpUpgradeHandler> T upgrade(final Class<T> handlerClass) throws IOException, ServletException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Helper method to decode a byte buffer into either UTF-8 or ISO-8859-1.
+ *
+ * @param buffer input buffer
+ *
+ * @return encoded String form of the data
+ */
+ @Nullable private static String decodeUnsafeString(final byte[] buffer) {
+
+ if (buffer == null) {
+ return null;
+ }
+
+ final ByteBuffer wrapper = ByteBuffer.wrap(buffer);
+
+ try {
+ return UTF_8.decode(wrapper).toString();
+ } catch (final CharacterCodingException e) {
+
+ }
+
+ try {
+ return ISO_8859_1.decode(wrapper).toString();
+ } catch (final CharacterCodingException e) {
+
+ }
+
+ return null;
+ }
+
+ /** Helper class cribbed from Spring. */
+ private static class BodyInputStream extends ServletInputStream {
+
+ /** Underlying stream. */
+ @Nonnull private final InputStream delegate;
+
+ /**
+ * Constructor.
+ *
+ * @param body body data
+ */
+ public BodyInputStream(final byte[] body) {
+ delegate = new ByteArrayInputStream(body);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFinished() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setReadListener(final ReadListener readListener) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read() throws IOException {
+ return delegate.read();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ return delegate.read(b, off, len);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read(final byte[] b) throws IOException {
+ return delegate.read(b);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long skip(final long n) throws IOException {
+ return delegate.skip(n);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int available() throws IOException {
+ return delegate.available();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public synchronized void mark(final int readlimit) {
+ delegate.mark(readlimit);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public synchronized void reset() throws IOException {
+ delegate.reset();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean markSupported() {
+ return delegate.markSupported();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletResponse.java b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletResponse.java
new file mode 100644
index 0000000..b9ff05b
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletResponse.java
@@ -0,0 +1,504 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.TimeZone;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Uses a {@link DDF} object to reflect an HTTP response back to a remote caller.
+ */
+ at NotThreadSafe
+public class RemotedHttpServletResponse implements HttpServletResponse {
+
+ /** Underlying object for remoted data. */
+ @Nonnull private final DDF obj;
+
+ /** Size of each character buffer for output. */
+ private int bufferSize;
+
+ /** Tracks committing of response. */
+ private boolean committed;
+
+ /** A materialized output stream. */
+ @Nullable private BodyOutputStream outputStream;
+
+ /**
+ * Constructor.
+ *
+ * @param ddf object to capture response
+ */
+ public RemotedHttpServletResponse(@Nonnull final DDF ddf) {
+ obj = Constraint.isNotNull(ddf, "DDF cannot be null");
+ obj.structure();
+ bufferSize = 1024;
+ }
+
+ /**
+ * Gets the underlying object containing the response data.
+ *
+ * @return remoted data object
+ */
+ @Nonnull public DDF getDDF() {
+ return obj;
+ }
+
+ /** {@inheritDoc} */
+ public String getCharacterEncoding() {
+ return "UTF-8";
+ }
+
+ /** {@inheritDoc} */
+ public String getContentType() {
+ return getHeader("Content-Type");
+ }
+
+ /** {@inheritDoc} */
+ public ServletOutputStream getOutputStream() throws IOException {
+ if (committed) {
+ throw new IllegalStateException("Response already committed");
+ }
+
+ if (outputStream == null) {
+ outputStream = new BodyOutputStream();
+ }
+ return outputStream;
+ }
+
+ /** {@inheritDoc} */
+ public PrintWriter getWriter() throws IOException {
+ return new PrintWriter(getOutputStream(), false, Charset.forName(getCharacterEncoding()));
+ }
+
+ /** {@inheritDoc} */
+ public void setCharacterEncoding(final String charset) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public void setContentLength(final int len) {
+ setIntHeader("Content-Length", len);
+ }
+
+ /** {@inheritDoc} */
+ public void setContentLengthLong(final long len) {
+ setHeader("Content-Length", Long.toString(len));
+ }
+
+ /** {@inheritDoc} */
+ public void setContentType(final String type) {
+ setHeader("Content-Type", type);
+ }
+
+ /** {@inheritDoc} */
+ public void setBufferSize(final int size) {
+ bufferSize = size;
+ }
+
+ /** {@inheritDoc} */
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ /** {@inheritDoc} */
+ public void flushBuffer() throws IOException {
+ // Just mark as committed to signal that status and headers are frozen.
+ committed = true;
+ }
+
+ /** {@inheritDoc} */
+ public void resetBuffer() {
+ if (committed) {
+ throw new IllegalStateException("Response already committed");
+ }
+
+ if (outputStream != null) {
+ outputStream.reset();
+ }
+ }
+
+ /** {@inheritDoc} */
+ public boolean isCommitted() {
+ return committed;
+ }
+
+ /** {@inheritDoc} */
+ public void reset() {
+ if (committed) {
+ throw new IllegalStateException("Response already committed");
+ }
+ outputStream = null;
+ obj.structure();
+ }
+
+ /** {@inheritDoc} */
+ public void setLocale(final Locale loc) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public Locale getLocale() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public void addCookie(final Cookie cookie) {
+ // The C++ side already manages SameSite independently, so we'll likely continue that.
+ final StringBuffer buffer = new StringBuffer(cookie.getName()).append('=');
+ if (cookie.getValue() != null) {
+ buffer.append(cookie.getValue());
+ }
+ if (cookie.getMaxAge() >= 0) {
+ buffer.append("; MaxAge=").append(cookie.getMaxAge());
+ }
+ if (cookie.getPath() != null) {
+ buffer.append("; ").append("Path=").append(cookie.getPath());
+ }
+ if (cookie.getDomain() != null) {
+ buffer.append("; ").append("Domain=").append(cookie.getDomain());
+ }
+ if (cookie.getSecure()) {
+ buffer.append("; Secure");
+ }
+ if (cookie.isHttpOnly()) {
+ buffer.append("; HttpOnly");
+ }
+ addHeader("Set-Cookie", buffer.toString());
+ }
+
+ /** {@inheritDoc} */
+ public boolean containsHeader(final String name) {
+ for (final DDF header : obj.getmember("headers").asList()) {
+ if (name.equals(header.name())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public String encodeURL(final String url) {
+ return url;
+ }
+
+ /** {@inheritDoc} */
+ public String encodeRedirectURL(final String url) {
+ return url;
+ }
+
+ /** {@inheritDoc} */
+ public String encodeUrl(final String url) {
+ return url;
+ }
+
+ /** {@inheritDoc} */
+ public String encodeRedirectUrl(final String url) {
+ return url;
+ }
+
+ /** {@inheritDoc} */
+ public void sendError(final int sc, final String msg) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public void sendError(final int sc) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ public void sendRedirect(final String location) throws IOException {
+ if (committed) {
+ throw new IllegalStateException("Response already committed");
+ }
+
+ obj.getmember("response").remove();
+ obj.addmember("redirect").string(location);
+ committed = true;
+ outputStream = null;
+ }
+
+ /** {@inheritDoc} */
+ public void setDateHeader(final String name, final long date) {
+ unsetHeader(name);
+ addDateHeader(name, date);
+ }
+
+ /** {@inheritDoc} */
+ public void addDateHeader(final String name, final long date) {
+ final SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
+ formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+ addHeader(name, formatter.format(Date.from(Instant.ofEpochMilli(date))));
+ }
+
+ /** {@inheritDoc} */
+ public void setHeader(final String name, final String value) {
+ unsetHeader(name);
+ addHeader(name, value);
+ }
+
+ /** {@inheritDoc} */
+ public void addHeader(final String name, final String value) {
+ getHeaderList().add(new DDF(name).string(value));
+ }
+
+ /** {@inheritDoc} */
+ public void setIntHeader(final String name, final int value) {
+ unsetHeader(name);
+ addIntHeader(name, value);
+ }
+
+ /** {@inheritDoc} */
+ public void addIntHeader(final String name, final int value) {
+ getHeaderList().add(new DDF(name).integer(value));
+ }
+
+ /** {@inheritDoc} */
+ public void setStatus(final int sc) {
+ obj.addmember("response.status").integer(sc);
+ }
+
+ /** {@inheritDoc} */
+ public void setStatus(final int sc, final String sm) {
+ setStatus(sc);
+ obj.addmember("response.status_message").string(sm);
+ }
+
+ /** {@inheritDoc} */
+ public int getStatus() {
+ final Integer i = obj.getmember("response.status").integer();
+ return i != null ? i : -1;
+ }
+
+ /** {@inheritDoc} */
+ public String getHeader(final String name) {
+ final Optional<DDF> header =
+ obj.getmember("headers").asList()
+ .stream()
+ .filter(ddf -> name.equalsIgnoreCase(ddf.name()))
+ .findFirst();
+ if (header.isPresent()) {
+ if (header.orElseThrow().isstring()) {
+ return header.orElseThrow().string();
+ }
+ return header.orElseThrow().integer().toString();
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Collection<String> getHeaders(final String name) {
+ return obj.getmember("headers").asList()
+ .stream()
+ .filter(ddf -> name.equalsIgnoreCase(ddf.name()))
+ .map(DDF::string)
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ /** {@inheritDoc} */
+ public Collection<String> getHeaderNames() {
+ return obj.getmember("headers").asList()
+ .stream()
+ .map(DDF::name)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ /**
+ * Removes any existing header(s) of this type.
+ *
+ * @param name name of header to remove
+ */
+ private void unsetHeader(final @Nonnull @NotEmpty String name) {
+
+ if (committed) {
+ throw new IllegalStateException("Response already committed");
+ }
+
+ // This is safe because the asList copy is divorced from the original list
+ // but the DDF child objects are the same.
+ obj.getmember("headers").asList()
+ .stream()
+ .filter(ddf -> name.equalsIgnoreCase(ddf.name()))
+ .forEach(DDF::remove);
+ }
+
+ /**
+ * Get the list node to which headers should be added.
+ *
+ * @return a pre-existing list or a new one for mutation.
+ */
+ @Nonnull private DDF getHeaderList() {
+
+ if (committed) {
+ throw new IllegalStateException("Response already committed");
+ }
+
+ final DDF headers = obj.getmember("headers");
+ if (headers.islist()) {
+ return headers;
+ }
+ return obj.addmember("headers").list();
+ }
+
+ /** Wrapper allowing use of containers of arrays. */
+ private static final class ByteArrayWrapper {
+
+ /** Current write position. */
+ private int offset;
+
+ /** Wrapped array. */
+ private final byte[] buffer;
+
+ /**
+ * Constructor.
+ *
+ * @param size buffer size
+ */
+ private ByteArrayWrapper(final int size) {
+ buffer = new byte[size];
+ offset = 0;
+ }
+
+ /**
+ * Gets the buffer.
+ *
+ * @return the buffer
+ */
+ @Nonnull private byte[] getBuffer() {
+ return buffer;
+ }
+
+ /**
+ * Gets the offset.
+ *
+ * @return the offset
+ */
+ private int getOffset() {
+ return offset;
+ }
+
+ /**
+ * Writes a byte to the buffer.
+ *
+ * @param b byte to write
+ *
+ * @return true iff the buffer was large enough to accommodate the byte
+ */
+ private boolean write(final int b) {
+ if (offset < buffer.length) {
+ buffer[offset++] = Integer.valueOf(b).byteValue();
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ /** A pseudo {@link ServletOutputStream} to catch output. */
+ private class BodyOutputStream extends ServletOutputStream {
+
+
+ /** Internal buffers. */
+ @Nonnull @NonnullElements private final ArrayList<ByteArrayWrapper> bufferList;
+
+ /** The currently filling buffer. */
+ @Nonnull private ByteArrayWrapper currentBuffer;
+
+ /** Constructor. */
+ public BodyOutputStream() {
+ currentBuffer = new ByteArrayWrapper(bufferSize);
+ bufferList = new ArrayList<>(1);
+ bufferList.add(currentBuffer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setWriteListener(final WriteListener writeListener) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write(final int b) throws IOException {
+ if (!currentBuffer.write(b)) {
+ currentBuffer = new ByteArrayWrapper(bufferSize);
+ bufferList.add(currentBuffer);
+ Constraint.isTrue(currentBuffer.write(b), "Fresh buffer cannot fail to accept data");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void flush() throws IOException {
+
+ int offset = 0;
+ final byte[] copy = new byte[((bufferList.size() - 1) * bufferSize) +
+ bufferList.get(bufferList.size() - 1).getOffset()];
+
+ for (final ByteArrayWrapper b : bufferList) {
+ System.arraycopy(b.getBuffer(), 0, copy, offset, b.getOffset());
+ offset += b.getOffset();
+ }
+ obj.addmember("response.data").unsafe_string(copy);
+ committed = true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void close() throws IOException {
+ flush();
+ }
+
+ /** Clear all data written. */
+ private void reset() {
+ currentBuffer = new ByteArrayWrapper(bufferSize);
+ bufferList.clear();
+ bufferList.add(currentBuffer);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/package-info.java b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/package-info.java
new file mode 100644
index 0000000..2cf59d4
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/utilities/java/support/ddf/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.
+ */
+
+/**
+ * Implementation of the Dynamic Dataflow abstraction used in the
+ * Service Provider for interprocess communication.
+ *
+ * <p>DDFs are somewhat JSON-like, but with a (subjectively) friendlier
+ * API and allow for arbitrary serialization. They support dynamic RPC
+ * interfaces that don't require pre-definition of a data contract or
+ * compilation of client/server stubs.</p>
+ */
+
+package net.shibboleth.utilities.java.support.ddf;
\ No newline at end of file
diff --git a/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFSupportTest.java b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFSupportTest.java
new file mode 100644
index 0000000..382b09b
--- /dev/null
+++ b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFSupportTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Ignore;
+import org.testng.annotations.Test;
+import org.w3c.dom.Document;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.xml.BasicParserPool;
+import net.shibboleth.utilities.java.support.xml.XMLParserException;
+
+/**
+ * Unit test for {@link DDFSupport}.
+ */
+ at Ignore
+public class DDFSupportTest {
+
+ @Nullable private BasicParserPool parserPool;
+
+ /**
+ * Init parser.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeClass
+ public void setUp() throws ComponentInitializationException {
+ parserPool = new BasicParserPool();
+ parserPool.initialize();
+ }
+
+ /**
+ * Teardown.
+ */
+ @AfterClass
+ public void tearDown() {
+ parserPool.destroy();
+ }
+
+ /**
+ * Test conversion.
+ *
+ * @throws IOException
+ * @throws XMLParserException
+ */
+ @Test
+ public void test() throws XMLParserException, IOException {
+ final Document doc = parserPool.parse(getClass().getResourceAsStream("zork.xml"));
+ final DDF ddf = DDFSupport.fromElement(doc.getDocumentElement());
+
+ Assert.assertNotNull(ddf);
+ Assert.assertEquals(ddf.name(), "root");
+ Assert.assertEquals(ddf.getmember("foo").string(), "bar");
+
+ final List<DDF> children = ddf.getmember(DDFSupport.CHILD_ELEMENTS_MEMBER).asList();
+ Assert.assertEquals(children.size(), 2);
+
+ DDF child = children.get(0);
+ Assert.assertEquals(child.name(), "zork");
+ Assert.assertEquals(child.getmember("zorkmids").integer(), 10);
+ Assert.assertEquals(child.getmember("underground").string(), "true");
+ Assert.assertTrue(child.getmember(DDFSupport.CONTENT_MEMBER).isnull());
+ Assert.assertTrue(child.getmember(DDFSupport.CHILD_ELEMENTS_MEMBER).isnull());
+
+ child = children.get(1);
+ Assert.assertEquals(child.name(), "frobnitz");
+ Assert.assertEquals(child.getmember(DDFSupport.CONTENT_MEMBER).string(), "grue");
+ Assert.assertTrue(child.getmember(DDFSupport.CHILD_ELEMENTS_MEMBER).isnull());
+
+ Assert.assertTrue(ddf.getmember(DDFSupport.CONTENT_MEMBER).isnull());
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFTest.java b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFTest.java
new file mode 100644
index 0000000..086697f
--- /dev/null
+++ b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/DDFTest.java
@@ -0,0 +1,480 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import static org.testng.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+import javax.annotation.Nonnull;
+
+import org.testng.annotations.Ignore;
+import org.testng.annotations.Test;
+
+import net.shibboleth.utilities.java.support.collection.Pair;
+
+/**
+ * DDF unit tests.
+ */
+ at Ignore
+public class DDFTest {
+
+ @Test
+ public void testConstructors() {
+ DDF obj = new DDF();
+ assertNull(obj.name());
+ assertTrue(obj.isnull());
+
+ obj = new DDF("foo");
+ assertEquals(obj.name(), "foo");
+ assertTrue(obj.isempty());
+
+ obj = new DDF("foo", "bar");
+ assertEquals(obj.name(), "foo");
+ assertTrue(obj.isstring());
+ assertEquals(obj.string(), "bar");
+
+ obj = new DDF("foo", 42);
+ assertEquals(obj.name(), "foo");
+ assertTrue(obj.isint());
+ assertEquals(obj.integer(), Integer.valueOf(42));
+
+ obj = new DDF("foo", 42000000000L);
+ assertEquals(obj.name(), "foo");
+ assertTrue(obj.islong());
+ assertEquals(obj.longinteger(), Long.valueOf(42000000000L));
+
+ obj = new DDF("foo", 42.42);
+ assertEquals(obj.name(), "foo");
+ assertTrue(obj.isfloat());
+ assertEquals(obj.floating(), Double.valueOf(42.42));
+ }
+
+ @Test
+ public void testConversions() throws UnsupportedEncodingException {
+ final DDF obj = new DDF("foo");
+ obj.string("bar");
+ assertTrue(obj.isstring());
+ assertEquals(obj.string(), "bar");
+ assertNull(obj.integer());
+ assertNull(obj.floating());
+
+ obj.string(42);
+ assertTrue(obj.isstring());
+ assertEquals(obj.string(), "42");
+ assertEquals(obj.integer(), Integer.valueOf(42));
+ assertEquals(obj.floating(), Double.valueOf(42));
+
+ obj.string(42.42);
+ assertTrue(obj.isstring());
+ assertEquals(obj.string(), "42.42");
+ assertNull(obj.integer());
+ assertEquals(obj.floating(), Double.valueOf(42.42));
+
+ obj.integer(42);
+ assertTrue(obj.isint());
+ assertEquals(obj.integer(), Integer.valueOf(42));
+ assertEquals(obj.floating(), Double.valueOf(42));
+
+ obj.integer("42");
+ assertTrue(obj.isint());
+ assertEquals(obj.integer(), Integer.valueOf(42));
+ assertEquals(obj.floating(), Double.valueOf(42));
+
+ obj.longinteger(42000000000L);
+ assertTrue(obj.islong());
+ assertEquals(obj.longinteger(), Long.valueOf(42000000000L));
+ assertEquals(obj.floating(), Double.valueOf(42000000000L));
+
+ obj.longinteger("42000000000");
+ assertTrue(obj.islong());
+ assertEquals(obj.longinteger(), Long.valueOf(42000000000L));
+ assertEquals(obj.floating(), Double.valueOf(42000000000L));
+
+ obj.floating(42.42);
+ assertTrue(obj.isfloat());
+ assertEquals(obj.integer(), Integer.valueOf(42));
+ assertEquals(obj.floating(), Double.valueOf(42.42));
+
+ obj.unsafe_string("bar".getBytes("ISO-8859-1"));
+ System.out.print(obj);
+ }
+
+ @Test
+ public void testLists() {
+ final DDF obj = new DDF().list();
+ assertTrue(obj.islist());
+ assertEquals(obj.integer(), Integer.valueOf(0));
+
+ obj.add(new DDF("foo", "bar"));
+ obj.add(new DDF("foo2", 42));
+ obj.add(new DDF("foo3").pointer(new Pair<>()));
+ assertEquals(obj.integer(), Integer.valueOf(3));
+
+ for (final DDF el : obj) {
+ switch (el.name()) {
+ case "foo":
+ assertEquals(el.string(), "bar");
+ break;
+
+ case "foo2":
+ assertEquals(el.integer(), Integer.valueOf(42));
+ break;
+
+ case "foo3":
+ assertEquals(el.pointer(), new Pair<>());
+ break;
+
+ default:
+ fail("Node unrecognized");
+ }
+ }
+
+ assertEquals(obj.getmember("[0]"), new DDF("foo", "bar"));
+ assertEquals(obj.getmember("[1]"), new DDF("foo2", 42));
+ assertTrue(obj.getmember("[3]").isnull());
+
+ obj.addafter(new DDF(null), obj.getmember("[0]"));
+ assertEquals(obj.integer(), Integer.valueOf(4));
+ assertTrue(obj.getmember("[1]").isempty());
+
+ obj.addbefore(new DDF("foo4"), obj.getmember("[2]"));
+ assertEquals(obj.integer(), Integer.valueOf(5));
+ assertTrue(obj.getmember("[2]").name().equals("foo4"));
+
+ assertTrue(obj.asList().get(4).remove().ispointer());
+ assertEquals(obj.integer(), Integer.valueOf(4));
+ }
+
+ @Test
+ public void testStructures() {
+ final DDF obj = new DDF().structure();
+ assertTrue(obj.isstruct());
+ assertEquals(obj.integer(), Integer.valueOf(0));
+
+ obj.add(new DDF("foo", "bar"));
+ assertEquals(obj.integer(), Integer.valueOf(1));
+ assertTrue(obj.getmember("foo").name().equals("foo"));
+ assertTrue(obj.getmember("foo").string().equals("bar"));
+
+ obj.addmember("foo2").integer(42);
+ assertEquals(obj.integer(), Integer.valueOf(2));
+
+ obj.addmember("foo2.foo3").string("bar3");
+ assertEquals(obj.integer(), Integer.valueOf(2));
+ assertTrue(obj.getmember("foo2").isstruct());
+ assertEquals(obj.getmember("foo2").integer(), Integer.valueOf(1));
+ assertTrue(obj.getmember("foo2").getmember("foo3").string().equals("bar3"));
+ assertTrue(obj.getmember("foo2.foo3").string().equals("bar3"));
+ }
+
+ @Test
+ public void testEncoder() throws IOException {
+ try (final ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
+ DDF.encode(sink, "foo".getBytes("UTF8"));
+ assertEquals(sink.toString(), "foo");
+ sink.reset();
+
+ DDF.encode(sink, "foo bar".getBytes("UTF8"));
+ assertEquals(sink.toString(), "foo%20bar");
+ sink.reset();
+
+ DDF.encode(sink, "foo\nbar".getBytes("UTF8"));
+ assertEquals(sink.toString(), "foo%0Abar");
+ sink.reset();
+
+ DDF.encode(sink, "foo☯️bar".getBytes("UTF8"));
+ assertEquals(sink.toString(), "foo%E2%98%AF%EF%B8%8Fbar");
+ sink.reset();
+
+ // -128 corresponds to 128, which is the extended ASCII Euro symbol.
+ // This test demonstrates that round-tripping through such an encoding
+ // will preserve the original 0x80 hex value in that position in the string
+ // rather than converting through the UTF-8 representation.
+ final byte[] unsafe = {102, 111, 111, -128, 98, 97, 114};
+ DDF.encode(sink, new String(unsafe, "ISO-8859-1").getBytes("ISO-8859-1"));
+ assertEquals(sink.toString(), "foo%80bar");
+ sink.reset();
+ }
+ }
+
+ @Test
+ public void testSerialize() throws IOException {
+
+ try (final ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
+ DDF obj = new DDF(null);
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("empty-noname.ddf"));
+ sink.reset();
+
+ obj.name("foo bar");
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("empty-name.ddf"));
+ sink.reset();
+
+ obj.string("zorkmid☯️");
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("string-name.ddf"));
+ sink.reset();
+
+ final byte[] unsafe = {102, 111, 111, -128, 98, 97, 114};
+ obj.unsafe_string(unsafe);
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("unsafestring-name.ddf"));
+ sink.reset();
+
+ obj.integer(42);
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("int-name.ddf"));
+ sink.reset();
+
+ obj.longinteger(42000000000L);
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("long-name.ddf"));
+ sink.reset();
+
+ obj.floating(42.1315927);
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("float-name.ddf"));
+ sink.reset();
+
+ obj.structure();
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("struct-empty.ddf"));
+ sink.reset();
+
+ obj.addmember("infocom.zork").list().add(new DDF().integer(1));
+ obj.getmember("infocom.zork").add(new DDF().integer(2));
+ obj.getmember("infocom.zork").add(new DDF().integer(3));
+ obj.serialize(sink);
+ assertEquals(sink.toByteArray(), testFile("struct-complex.ddf"));
+ sink.reset();
+ }
+ }
+
+ /**
+ * Convert test file contents to a byte array.
+ *
+ * @param name file name
+ *
+ * @return byte array
+ *
+ * @throws IOException on error
+ */
+ private byte[] testFile(@Nonnull final String name) throws IOException {
+ return getClass().getResourceAsStream(name).readAllBytes();
+ }
+
+ @Test
+ public void testDeserialize() throws IOException {
+ try (final InputStream is = getClass().getResourceAsStream("empty-noname.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isempty());
+ assertNull(obj.name());
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("empty-name.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isempty());
+ assertEquals(obj.name(), "foo bar");
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("string-name.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isstring());
+ assertEquals(obj.name(), "foo bar");
+ assertEquals(obj.string(), "zorkmid☯️");
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("unsafestring-name.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isunsafestring());
+ assertEquals(obj.name(), "foo bar");
+ final byte[] unsafe = {102, 111, 111, -128, 98, 97, 114};
+ assertEquals(obj.unsafe_string(), unsafe);
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("int-name.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isint());
+ assertEquals(obj.name(), "foo bar");
+ assertEquals(obj.integer(), Integer.valueOf(42));
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("long-name.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.islong());
+ assertEquals(obj.name(), "foo bar");
+ assertEquals(obj.longinteger(), Long.valueOf(42000000000L));
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("float-name.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isfloat());
+ assertEquals(obj.name(), "foo bar");
+ assertEquals(obj.floating(), Double.valueOf(42.1315927));
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("struct-empty.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isstruct());
+ assertEquals(obj.name(), "foo bar");
+ assertEquals(obj.integer(), Integer.valueOf(0));
+ }
+
+ try (final InputStream is = getClass().getResourceAsStream("struct-complex.ddf")) {
+ final DDF obj = DDF.deserialize(is);
+ assertTrue(obj.isstruct());
+ assertEquals(obj.name(), "foo bar");
+ assertEquals(obj.integer(), Integer.valueOf(1));
+ assertTrue(obj.getmember("infocom").isstruct());
+ assertTrue(obj.getmember("infocom.zork").islist());
+ assertEquals(obj.getmember("infocom.zork").integer(), Integer.valueOf(3));
+ assertEquals(obj.getmember("infocom.zork.[0]").integer(), Integer.valueOf(1));
+ assertEquals(obj.getmember("infocom.zork.[1]").integer(), Integer.valueOf(2));
+ assertEquals(obj.getmember("infocom.zork.[2]").integer(), Integer.valueOf(3));
+ }
+ }
+
+ @Test
+ public void testBadInputs() {
+ try (final InputStream is = new ByteArrayInputStream(new String().getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String("\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(" ").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(".\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". \n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". -2").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 0 \n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 1 foo \n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 2\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 2 \n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 3\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 3 \n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 4 \n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 4\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 4 2\n. 1 foo\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 5 foo\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+
+ try (final InputStream is = new ByteArrayInputStream(new String(". 5 1\n").getBytes("UTF-8"))) {
+ DDF.deserialize(is);
+ fail("Should have thrown IOException");
+ } catch (final IOException e) {
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletRequestTest.java b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletRequestTest.java
new file mode 100644
index 0000000..255d8d2
--- /dev/null
+++ b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletRequestTest.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import static org.testng.Assert.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import jakarta.servlet.http.Cookie;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Ignore;
+import org.testng.annotations.Test;
+import org.testng.reporters.Files;
+
+/**
+ * Unit test for {@link RemotedHttpServletRequest}.
+ */
+ at Ignore
+public class RemotedHttpServletRequestTest {
+
+ private DDF obj;
+ private RemotedHttpServletRequest req;
+
+ @BeforeMethod
+ public void setUp() {
+ obj = new DDF();
+ req = new RemotedHttpServletRequest(obj);
+ }
+
+ @Test
+ public void testEmpty() throws IOException {
+ assertEquals(req.getContentLength(), -1);
+ assertEquals(req.getContentType(), null);
+ assertEquals(req.getCookies(), null);
+ assertEquals(req.getHeader("foo"), null);
+ assertEquals(req.getInputStream().read(), -1);
+ assertEquals(req.getMethod(), null);
+ assertEquals(req.getParameter("foo"), null);
+ assertEquals(req.getQueryString(), null);
+ assertEquals(req.getRemoteAddr(), null);
+ assertEquals(req.getRemoteUser(), null);
+ assertEquals(req.getRequestURI(), null);
+ assertEquals(req.getRequestURL().toString(), "");
+ assertEquals(req.getScheme(), null);
+ assertFalse(req.isSecure());
+ assertEquals(req.getServerPort(), -1);
+ }
+
+ @Test
+ public void testBasics() throws IOException {
+ obj.structure();
+ obj.addmember("content_length").integer(100);
+ obj.addmember("content_type").string("text/xml");
+ obj.addmember("method").string("POST");
+ obj.addmember("body").string("<foo/>");
+ obj.addmember("port").integer(80);
+ obj.addmember("client_addr").string("127.0.0.1");
+ obj.addmember("remote_user").string("jdoe");
+ obj.addmember("hostname").unsafe_string("localhost".getBytes("UTF-8"));
+ obj.addmember("uri").unsafe_string("/endpoint".getBytes("UTF-8"));
+ obj.addmember("url").unsafe_string("http://localhost/endpoint".getBytes("UTF-8"));
+ obj.addmember("scheme").string("http");
+
+ assertEquals(req.getContentLength(), 100);
+ assertEquals(req.getContentType(), "text/xml");
+ assertEquals(req.getCookies(), null);
+ assertEquals(req.getHeader("foo"), null);
+ assertEquals(Files.streamToString(req.getInputStream()), "<foo/>");
+ assertEquals(req.getMethod(), "POST");
+ assertEquals(req.getParameter("foo"), null);
+ assertEquals(req.getQueryString(), null);
+ assertEquals(req.getRemoteAddr(), "127.0.0.1");
+ assertEquals(req.getRemoteUser(), "jdoe");
+ assertEquals(req.getServerName(), "localhost");
+ assertEquals(req.getRequestURI(), "/endpoint");
+ assertEquals(req.getRequestURL().toString(), "http://localhost/endpoint");
+ assertEquals(req.getScheme(), "http");
+ assertFalse(req.isSecure());
+ assertEquals(req.getServerPort(), 80);
+ }
+
+ @Test
+ public void testOneQueryParameter() throws IOException {
+ obj.structure();
+ obj.addmember("content_type").string("text/xml");
+ obj.addmember("body").string("<foo/>");
+ obj.addmember("query").string("foo=bar+baz");
+
+ assertEquals(req.getParameterNames().nextElement(), "foo");
+ assertEquals(req.getParameter("foo"), "bar baz");
+ }
+
+ @Test
+ public void testMultiQueryParameters() throws IOException {
+ obj.structure();
+ obj.addmember("content_type").string("text/xml");
+ obj.addmember("body").string("<foo/>");
+ obj.addmember("query").string("foo=bar+baz&zork=grue&foo=baf");
+
+ assertEquals(req.getParameter("foo"), "bar baz");
+ assertEquals(req.getParameterValues("foo"), List.of("bar baz", "baf").toArray());
+ assertEquals(req.getParameter("zork"), "grue");
+ }
+
+ @Test
+ public void testFormParameters() throws IOException {
+ obj.structure();
+ obj.addmember("content_type").string("application/x-www-form-urlencoded");
+ obj.addmember("body").string("foo=baf");
+ obj.addmember("query").string("foo=bar+baz&zork=grue");
+
+ assertEquals(req.getParameter("foo"), "bar baz");
+ assertEquals(req.getParameterValues("foo"), List.of("bar baz", "baf").toArray());
+ assertEquals(req.getParameter("zork"), "grue");
+ }
+
+ @Test
+ public void testHeaders() throws IOException {
+ obj.structure();
+ obj.addmember("headers.foo").unsafe_string("bar".getBytes("UTF-8"));
+ obj.addmember("headers.zork").unsafe_string("grue".getBytes("UTF-8"));
+
+ assertEquals(req.getHeaders("foo").nextElement(), "bar");
+ assertEquals(req.getHeader("zork"), "grue");
+ assertNull(req.getHeader("baz"));
+ }
+
+ @Test
+ public void testCookie() throws IOException {
+ obj.structure();
+ obj.addmember("headers.Cookie").unsafe_string("foo=bar;".getBytes("UTF-8"));
+
+ final Cookie[] cookies = req.getCookies();
+
+ assertEquals(cookies.length, 1);
+ assertEquals(cookies[0].getName(), "foo");
+ assertEquals(cookies[0].getValue(), "bar");
+ }
+
+ @Test
+ public void testCookies() throws IOException {
+ obj.structure();
+ obj.addmember("headers.Cookie").unsafe_string("foo=bar; zork=grue".getBytes("UTF-8"));
+
+ final Cookie[] cookies = req.getCookies();
+
+ assertEquals(cookies.length, 2);
+ assertEquals(cookies[0].getName(), "foo");
+ assertEquals(cookies[0].getValue(), "bar");
+ assertEquals(cookies[1].getName(), "zork");
+ assertEquals(cookies[1].getValue(), "grue");
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletResponseTest.java b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletResponseTest.java
new file mode 100644
index 0000000..5853dac
--- /dev/null
+++ b/sp-server-api/src/test/java/net/shibboleth/utilities/java/support/ddf/RemotedHttpServletResponseTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.utilities.java.support.ddf;
+
+import static org.testng.Assert.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.Set;
+
+import jakarta.servlet.http.Cookie;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Ignore;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link RemotedHttpServletResponse}.
+ */
+ at Ignore
+public class RemotedHttpServletResponseTest {
+
+ private DDF obj;
+ private RemotedHttpServletResponse resp;
+
+ @BeforeMethod
+ public void setUp() {
+ obj = new DDF();
+ resp = new RemotedHttpServletResponse(obj);
+ }
+
+ @Test
+ public void testBasics() throws IOException {
+
+ resp.setContentType("text/xml");
+ resp.setHeader("Cache-Control", "private");
+ resp.addHeader("foo", "foo");
+ resp.setHeader("Foo", "bar");
+ resp.addDateHeader("Foo", Instant.now().toEpochMilli());
+ resp.addIntHeader("Bar", 42);
+ resp.setContentLength(42);
+
+ final Cookie cookie = new Cookie("cookie1", "value1");
+ cookie.setSecure(true);
+ cookie.setHttpOnly(true);
+ cookie.setPath("/idp");
+ resp.addCookie(cookie);
+
+ assertEquals(resp.getContentType(), "text/xml");
+ assertEquals((Set<String>) resp.getHeaderNames(), Set.of("Content-Type", "Cache-Control", "Foo", "Bar", "Content-Length", "Set-Cookie"));
+ assertTrue(resp.getHeaders("foo").contains("bar"));
+ assertEquals(resp.getHeader("Bar"), "42");
+
+ final DDF headers = obj.getmember("headers");
+ assertTrue(headers.islist());
+ final DDF cheader = headers.asList().stream().filter(ddf -> "Set-Cookie".equalsIgnoreCase(ddf.name())).findFirst().orElseThrow();
+ assertEquals(cheader.string(), "cookie1=value1; Path=/idp; Secure; HttpOnly");
+ }
+
+ @Test
+ public void testRedirect() throws IOException {
+ resp.sendRedirect("http://localhost");
+
+ assertTrue(resp.isCommitted());
+ assertEquals(obj.getmember("redirect").string(), "http://localhost");
+
+ try {
+ resp.getOutputStream();
+ fail("Response should have been committed.");
+ } catch (final IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void testResponseStream() throws IOException {
+ resp.setBufferSize(3);
+ resp.setStatus(200);
+ try (final OutputStream os = resp.getOutputStream()) {
+ os.write("zorkmid".getBytes("UTF-8"));
+ }
+
+ assertTrue(resp.isCommitted());
+ assertEquals(obj.getmember("response.status").integer(), Integer.valueOf(200));
+ assertEquals(obj.getmember("response.data").unsafe_string(), "zorkmid".getBytes("UTF-8"));
+ }
+
+ @Test
+ public void testResponseStream2() throws IOException {
+ resp.setBufferSize(3);
+ resp.setStatus(200);
+ try (final OutputStream os = resp.getOutputStream()) {
+ os.write("zorkmid☯️".getBytes("ISO-8859-1"));
+ }
+
+ assertTrue(resp.isCommitted());
+ assertEquals(obj.getmember("response.status").integer(), Integer.valueOf(200));
+ assertEquals(obj.getmember("response.data").unsafe_string(), "zorkmid☯️".getBytes("ISO-8859-1"));
+ }
+
+ @Test
+ public void testWriter() throws IOException {
+ resp.setBufferSize(3);
+ resp.setStatus(200);
+ try (final PrintWriter pw = resp.getWriter()) {
+ pw.print("zorkmid");
+ }
+
+ assertTrue(resp.isCommitted());
+ assertEquals(obj.getmember("response.status").integer(), Integer.valueOf(200));
+ assertEquals(obj.getmember("response.data").unsafe_string(), "zorkmid".getBytes("UTF-8"));
+ }
+
+ @Test
+ public void testWriter2() throws IOException {
+ resp.setBufferSize(3);
+ resp.setStatus(200);
+ try (final PrintWriter pw = resp.getWriter()) {
+ pw.print("zorkmid☯️");
+ }
+
+ assertTrue(resp.isCommitted());
+ assertEquals(obj.getmember("response.status").integer(), Integer.valueOf(200));
+ assertEquals(obj.getmember("response.data").unsafe_string(), "zorkmid☯️".getBytes("UTF-8"));
+ }
+}
\ No newline at end of file
diff --git a/sp-server-impl/pom.xml b/sp-server-impl/pom.xml
index 83eeebc..ee0df5f 100644
--- a/sp-server-impl/pom.xml
+++ b/sp-server-impl/pom.xml
@@ -27,18 +27,6 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
- <dependency>
- <groupId>net.shibboleth.utilities</groupId>
- <artifactId>java-support</artifactId>
- <version>${java-support.version}</version>
- <scope>compile</scope>
- </dependency>
- <dependency>
- <groupId>net.shibboleth.ext</groupId>
- <artifactId>spring-extensions</artifactId>
- <version>${spring-extensions.version}</version>
- <scope>compile</scope>
- </dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-core</artifactId>
@@ -78,19 +66,6 @@
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>net.shibboleth.ext</groupId>
- <artifactId>spring-extensions</artifactId>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>net.shibboleth.utilities</groupId>
- <artifactId>java-support</artifactId>
- <version>${java-support.version}</version>
- <type>test-jar</type>
- <scope>test</scope>
- </dependency>
</dependencies>
</project>
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
index 390beb0..3e5a1fd 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
@@ -31,6 +31,7 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.ddf.DDF;
import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.service.ReloadableService;
/**
* Basic implementation of an {@link Application}.
@@ -43,6 +44,18 @@ public class BasicApplication extends AbstractEndpoint implements Application {
/** Component endpoints. */
@NonnullAfterInit private ApplicationEndpointManager endpointManager;
+ /** Metadata source. */
+ @NonnullAfterInit private ReloadableService<Object> metadataResolver;
+
+ /** Transcoding registry. */
+ @NonnullAfterInit private ReloadableService<Object> transcodingRegistry;
+
+ /** Attribute source. */
+ @NonnullAfterInit private ReloadableService<Object> attributeResolver;
+
+ /** Filter engine. */
+ @NonnullAfterInit private ReloadableService<Object> attributeFilter;
+
/**
* Sets the {@link ApplicationEndpointManager} to use.
*
@@ -62,6 +75,8 @@ public class BasicApplication extends AbstractEndpoint implements Application {
if (endpointManager == null) {
throw new ComponentInitializationException("ApplicationEndpointManager cannot be null");
}
+
+ // TODO: enforce services or install default versions?
}
/** {@inheritDoc} */
@@ -69,6 +84,37 @@ public class BasicApplication extends AbstractEndpoint implements Application {
return getId();
}
+ /** {@inheritDoc} */
+ @Nonnull public ReloadableService<Object> getMetadataResolver() {
+ return metadataResolver;
+ }
+
+ /**
+ * Set the {@link MetadataResolver} to use.
+ *
+ * @param service metadata resolver service
+ */
+ public void setMetadataResolver(@Nonnull final ReloadableService<Object> service) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ metadataResolver = Constraint.isNotNull(service, "MetadataResolver service cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull public ReloadableService<Object> getAttributeTranscoderRegistry() {
+ return transcodingRegistry;
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull public ReloadableService<Object> getAttributeResolver() {
+ return attributeResolver;
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull public ReloadableService<Object> getAttributeFilter() {
+ return attributeFilter;
+ }
+
/** {@inheritDoc} */
@Override
@Nonnull public DDF doReceive(@Nonnull final DDF input) throws RemoteProcessingException {
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list