From 53a2b8dff6d5ad71706dffc795acbc003b1060eb Mon Sep 17 00:00:00 2001 From: Philip Helger Date: Thu, 5 Dec 2024 20:35:56 +0100 Subject: [PATCH] Serializing and restoring ValidationSource in ValidationResultList --- README.md | 2 + .../phive/api/source/IValidationSource.java | 37 ++++++ .../api/source/IValidationSourceBinary.java | 2 + .../api/source/ValidationSourceBinary.java | 40 +++++- .../result/IValidationSourceRestorer.java | 37 ++++++ .../phive/result/PhiveResultHelper.java | 36 ++++++ .../json/JsonValidationResultListHelper.java | 18 +++ .../phive/result/json/PhiveJsonHelper.java | 113 +++++++++++++++-- .../phive/result/xml/PhiveXMLHelper.java | 114 ++++++++++++++++-- .../xml/XMLValidationResultListHelper.java | 19 +++ .../result/json/PhiveJsonHelperTest.java | 3 + .../xml/source/IValidationSourceXML.java | 4 +- .../phive/xml/source/ValidationSourceXML.java | 33 ++--- .../ValidationSourceXMLReadableResource.java | 63 ++++++++++ 14 files changed, 487 insertions(+), 34 deletions(-) create mode 100644 phive-result/src/main/java/com/helger/phive/result/IValidationSourceRestorer.java create mode 100644 phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXMLReadableResource.java diff --git a/README.md b/README.md index 918597e1..cd9c4e3d 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ Please ensure that your stack size is at least 1MB (for Saxon). Using the Oracle * Added new class `ValidityDeterminatorRegistry` to support validity determination processes * Added optional `IValidationSource` to `ValidationResultList` * Changed parameter types in `PhiveJsonHelper` and `PhiveXMLHelper` from `List ` to `ValidationResultList` + * Added new class `ValidationSourceXMLReadableResource` + * Extended `IValidationSource` interface to make content serializable * v10.0.3 - 2024-12-01 * Added default XML serialization of validation results * Added new `EValidationBaseType` entries diff --git a/phive-api/src/main/java/com/helger/phive/api/source/IValidationSource.java b/phive-api/src/main/java/com/helger/phive/api/source/IValidationSource.java index b05b41ac..fc5c272c 100644 --- a/phive-api/src/main/java/com/helger/phive/api/source/IValidationSource.java +++ b/phive-api/src/main/java/com/helger/phive/api/source/IValidationSource.java @@ -16,7 +16,15 @@ */ package com.helger.phive.api.source; +import java.io.IOException; +import java.io.OutputStream; + +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.annotation.WillNotClose; + +import com.helger.commons.annotation.Nonempty; +import com.helger.commons.string.StringHelper; /** * Abstract validation source interface. This represents an object to be @@ -26,6 +34,24 @@ */ public interface IValidationSource { + /** + * @return The validation source type ID. Neither null nor empty. + * @since 10.1.0 + */ + @Nonnull + @Nonempty + String getValidationSourceTypeID (); + + /** + * @return true if a system ID is present, false if + * not. + * @since 10.1.0 + */ + default boolean hasSystemID () + { + return StringHelper.hasText (getSystemID ()); + } + /** * @return The system ID (e.g. filename) of the source to be validated. May be * null. @@ -39,4 +65,15 @@ public interface IValidationSource * be a way to define the necessary part(s) in the implementation. */ boolean isPartialSource (); + + /** + * Write the content of the validation source to the provided OutputStream. + * + * @param aOS + * The output stream to write to. May not be null. + * @throws IOException + * In case writing fails + * @since 10.1.0 + */ + void writeTo (@Nonnull @WillNotClose OutputStream aOS) throws IOException; } diff --git a/phive-api/src/main/java/com/helger/phive/api/source/IValidationSourceBinary.java b/phive-api/src/main/java/com/helger/phive/api/source/IValidationSourceBinary.java index 029c3f6f..7b4a2b3d 100644 --- a/phive-api/src/main/java/com/helger/phive/api/source/IValidationSourceBinary.java +++ b/phive-api/src/main/java/com/helger/phive/api/source/IValidationSourceBinary.java @@ -28,6 +28,8 @@ */ public interface IValidationSourceBinary extends IValidationSource { + String VALIDATION_SOURCE_TYPE = "binary"; + /** * @return The bytes to be validated. May be null. */ diff --git a/phive-api/src/main/java/com/helger/phive/api/source/ValidationSourceBinary.java b/phive-api/src/main/java/com/helger/phive/api/source/ValidationSourceBinary.java index 669a830a..e2453d1e 100644 --- a/phive-api/src/main/java/com/helger/phive/api/source/ValidationSourceBinary.java +++ b/phive-api/src/main/java/com/helger/phive/api/source/ValidationSourceBinary.java @@ -16,10 +16,15 @@ */ package com.helger.phive.api.source; +import java.io.IOException; +import java.io.OutputStream; + import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.annotation.WillNotClose; import com.helger.commons.ValueEnforcer; +import com.helger.commons.annotation.Nonempty; import com.helger.commons.io.ByteArrayWrapper; import com.helger.commons.string.ToStringGenerator; @@ -35,7 +40,9 @@ public class ValidationSourceBinary implements IValidationSourceBinary private final boolean m_bPartialSource; private final ByteArrayWrapper m_aBAW; - protected ValidationSourceBinary (@Nullable final String sSystemID, @Nonnull final ByteArrayWrapper aBAW, final boolean bPartialSource) + protected ValidationSourceBinary (@Nullable final String sSystemID, + @Nonnull final ByteArrayWrapper aBAW, + final boolean bPartialSource) { ValueEnforcer.notNull (aBAW, "BAW"); m_sSystemID = sSystemID; @@ -43,6 +50,13 @@ protected ValidationSourceBinary (@Nullable final String sSystemID, @Nonnull fin m_bPartialSource = bPartialSource; } + @Nonnull + @Nonempty + public String getValidationSourceTypeID () + { + return VALIDATION_SOURCE_TYPE; + } + @Nullable public String getSystemID () { @@ -60,6 +74,12 @@ public ByteArrayWrapper getBytes () return m_aBAW; } + public void writeTo (@Nonnull @WillNotClose final OutputStream aOS) throws IOException + { + // Just forward + m_aBAW.writeTo (aOS); + } + @Override public String toString () { @@ -84,4 +104,22 @@ public static ValidationSourceBinary create (@Nullable final String sSystemID, @ ValueEnforcer.notNull (aBytes, "Bytes"); return new ValidationSourceBinary (sSystemID, new ByteArrayWrapper (aBytes, false), false); } + + /** + * Create a partial validation source from an existing byte array. + * + * @param sSystemID + * System ID to use. May be null. + * @param aBytes + * The bytes to use. May not be null. + * @return Never null. + * @since 10.1.0 + */ + @Nonnull + public static ValidationSourceBinary createPartial (@Nullable final String sSystemID, @Nonnull final byte [] aBytes) + { + ValueEnforcer.notNull (aBytes, "Bytes"); + return new ValidationSourceBinary (sSystemID, new ByteArrayWrapper (aBytes, false), true); + } + } diff --git a/phive-result/src/main/java/com/helger/phive/result/IValidationSourceRestorer.java b/phive-result/src/main/java/com/helger/phive/result/IValidationSourceRestorer.java new file mode 100644 index 00000000..178b2679 --- /dev/null +++ b/phive-result/src/main/java/com/helger/phive/result/IValidationSourceRestorer.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014-2024 Philip Helger (www.helger.com) + * philip[at]helger[dot]com + * + * Licensed 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 com.helger.phive.result; + +import javax.annotation.Nullable; + +import com.helger.phive.api.source.IValidationSource; + +/** + * Callback interface to restore a {@link IValidationSource} from deserialized + * parameters. + * + * @author Philip Helger + * @since 10.1.0 + */ +public interface IValidationSourceRestorer +{ + @Nullable + IValidationSource restoreValidationSource (@Nullable String sValidationSourceTypeID, + @Nullable String sSystemID, + boolean bIsPartialSource, + @Nullable byte [] aPayloadBytes); +} diff --git a/phive-result/src/main/java/com/helger/phive/result/PhiveResultHelper.java b/phive-result/src/main/java/com/helger/phive/result/PhiveResultHelper.java index 1c0ec389..f38e6c35 100644 --- a/phive-result/src/main/java/com/helger/phive/result/PhiveResultHelper.java +++ b/phive-result/src/main/java/com/helger/phive/result/PhiveResultHelper.java @@ -22,6 +22,9 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.helger.commons.ValueEnforcer; import com.helger.commons.annotation.Nonempty; import com.helger.commons.error.level.EErrorLevel; @@ -34,6 +37,12 @@ import com.helger.commons.io.resource.wrapped.IWrappedReadableResource; import com.helger.commons.state.ETriState; import com.helger.commons.string.StringHelper; +import com.helger.phive.api.source.IValidationSource; +import com.helger.phive.api.source.IValidationSourceBinary; +import com.helger.phive.api.source.ValidationSourceBinary; +import com.helger.phive.xml.source.IValidationSourceXML; +import com.helger.phive.xml.source.ValidationSourceXML; +import com.helger.xml.serialize.read.DOMReader; /** * Contains stateless phive result helper methods. @@ -51,6 +60,8 @@ public final class PhiveResultHelper public static final String VALUE_TRISTATE_FALSE = "FALSE"; public static final String VALUE_TRISTATE_UNDEFINED = "UNDEFINED"; + private static final Logger LOGGER = LoggerFactory.getLogger (PhiveResultHelper.class); + private PhiveResultHelper () {} @@ -198,4 +209,29 @@ public static IReadableResource getAsValidationResource (@Nullable final String // Default to class path return new ClassPathResource (sArtefactPath); } + + @Nullable + public static IValidationSource createValidationSource (@Nullable final String sValidationSourceTypeID, + @Nullable final String sSystemID, + final boolean bIsPartialSource, + @Nullable final byte [] aPayloadBytes) + { + if (StringHelper.hasNoText (sValidationSourceTypeID)) + return null; + if (aPayloadBytes == null) + return null; + + switch (sValidationSourceTypeID) + { + case IValidationSourceBinary.VALIDATION_SOURCE_TYPE: + return bIsPartialSource ? ValidationSourceBinary.createPartial (sSystemID, aPayloadBytes) + : ValidationSourceBinary.create (sSystemID, aPayloadBytes); + case IValidationSourceXML.VALIDATION_SOURCE_TYPE: + // Parse on demand only + return new ValidationSourceXML (sSystemID, () -> DOMReader.readXMLDOM (aPayloadBytes), bIsPartialSource); + default: + LOGGER.warn ("Unsupported Validation Source Type ID '" + sValidationSourceTypeID + "'"); + } + return null; + } } diff --git a/phive-result/src/main/java/com/helger/phive/result/json/JsonValidationResultListHelper.java b/phive-result/src/main/java/com/helger/phive/result/json/JsonValidationResultListHelper.java index ce2bb234..2379bde7 100644 --- a/phive-result/src/main/java/com/helger/phive/result/json/JsonValidationResultListHelper.java +++ b/phive-result/src/main/java/com/helger/phive/result/json/JsonValidationResultListHelper.java @@ -38,6 +38,7 @@ import com.helger.phive.api.executorset.IValidationExecutorSet; import com.helger.phive.api.result.ValidationResult; import com.helger.phive.api.result.ValidationResultList; +import com.helger.phive.api.source.IValidationSource; import com.helger.phive.api.validity.EExtendedValidity; import com.helger.phive.result.PhiveResultHelper; @@ -50,6 +51,9 @@ */ public class JsonValidationResultListHelper { + // By default, include source content + private Function m_aSourceToJson = vs -> PhiveJsonHelper.getJsonValidationSource (vs, + true); private IValidationExecutorSet m_aVES; private Function , IJsonObject> m_aVESToJson = PhiveJsonHelper::getJsonVES; private Function m_aArtifactPathTypeToJson = PhiveResultHelper::getArtifactPathType; @@ -61,6 +65,13 @@ public class JsonValidationResultListHelper public JsonValidationResultListHelper () {} + @Nonnull + public JsonValidationResultListHelper sourceToJson (@Nullable final Function a) + { + m_aSourceToJson = a; + return this; + } + @Nonnull public JsonValidationResultListHelper ves (@Nullable final IValidationExecutorSet a) { @@ -154,6 +165,13 @@ public void applyTo (@Nonnull final IJsonObject aResponse, ValueEnforcer.notNull (aDisplayLocale, "DisplayLocale"); ValueEnforcer.isGE0 (nDurationMilliseconds, "DurationMilliseconds"); + // Added in 10.1.0 + if (aValidationResultList.hasValidationSource () && m_aSourceToJson != null) + { + aResponse.addIfNotNull (PhiveJsonHelper.JSON_VALIDATION_SOURCE, + m_aSourceToJson.apply (aValidationResultList.getValidationSource ())); + } + if (m_aVES != null && m_aVESToJson != null) aResponse.addIfNotNull (PhiveJsonHelper.JSON_VES, m_aVESToJson.apply (m_aVES)); diff --git a/phive-result/src/main/java/com/helger/phive/result/json/PhiveJsonHelper.java b/phive-result/src/main/java/com/helger/phive/result/json/PhiveJsonHelper.java index f9776a3e..ca96efaf 100644 --- a/phive-result/src/main/java/com/helger/phive/result/json/PhiveJsonHelper.java +++ b/phive-result/src/main/java/com/helger/phive/result/json/PhiveJsonHelper.java @@ -16,8 +16,9 @@ */ package com.helger.phive.result.json; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; -import java.util.List; import java.util.Locale; import java.util.function.Function; @@ -31,6 +32,8 @@ import com.helger.commons.ValueEnforcer; import com.helger.commons.annotation.Nonempty; +import com.helger.commons.base64.Base64; +import com.helger.commons.base64.Base64OutputStream; import com.helger.commons.datetime.PDTWebDateHelper; import com.helger.commons.error.IError; import com.helger.commons.error.SingleError; @@ -39,6 +42,7 @@ import com.helger.commons.error.list.ErrorList; import com.helger.commons.error.text.ConstantHasErrorText; import com.helger.commons.io.resource.IReadableResource; +import com.helger.commons.io.stream.NonBlockingByteArrayOutputStream; import com.helger.commons.lang.StackTraceHelper; import com.helger.commons.location.ILocation; import com.helger.commons.location.SimpleLocation; @@ -62,6 +66,7 @@ import com.helger.phive.api.result.ValidationResultList; import com.helger.phive.api.source.IValidationSource; import com.helger.phive.api.validity.EExtendedValidity; +import com.helger.phive.result.IValidationSourceRestorer; import com.helger.phive.result.PhiveResultHelper; import com.helger.phive.result.exception.PhiveRestoredException; import com.helger.schematron.svrl.SVRLResourceError; @@ -69,7 +74,7 @@ /** * A utility class to create a common JSON representation of a PHIVE result. Use * {@link #applyGlobalError(IJsonObject, String, long)} or - * {@link #applyValidationResultList(IJsonObject, IValidationExecutorSet, List, Locale, long, MutableInt, MutableInt)} + * {@link #applyValidationResultList(IJsonObject, IValidationExecutorSet, ValidationResultList, Locale, long, MutableInt, MutableInt)} * to add the result to an arbitrary {@link IJsonObject}. * * @author Philip Helger @@ -106,6 +111,13 @@ public final class PhiveJsonHelper public static final String JSON_EXCEPTION = "exception"; public static final String JSON_TEST = "test"; + // Added in 10.1.0 + public static final String JSON_VALIDATION_SOURCE = "validationSource"; + public static final String JSON_SOURCE_TYPE_ID = "sourceTypeID"; + public static final String JSON_SYSTEM_ID = "systemID"; + public static final String JSON_PARTIAL_SOURCE = "partialSource"; + public static final String JSON_PAYLOAD = "payload"; + public static final String JSON_VESID = "vesid"; public static final String JSON_NAME = "name"; public static final String JSON_DEPRECATED = "deprecated"; @@ -466,16 +478,62 @@ public static IError getAsIError (@Nonnull final IJsonObject aObj) aLinkedException); } + /** + * Create the Validation Source details as an JSON Object.
+ * + *
+   * {
+   *   "sourceTypeID" : string,
+   *   "systemID" : string?,
+   *   "partialSource" : boolean,
+   *   "payload" : base64?
+   * }
+   * 
+ * + * @param aSource + * The validation source to use. May not be null. empty. + * @param bWithPayload + * true to include the payload, or false to + * omit it. + * @return The created JSON object. + * @since 10.1.0 + */ + @Nonnull + public static IJsonObject getJsonValidationSource (@Nonnull final IValidationSource aSource, + final boolean bWithPayload) + { + ValueEnforcer.notNull (aSource, "Source"); + + final IJsonObject ret = new JsonObject ().add (JSON_SOURCE_TYPE_ID, aSource.getValidationSourceTypeID ()) + .addIfNotNull (JSON_SYSTEM_ID, aSource.getSystemID ()) + .add (JSON_PARTIAL_SOURCE, aSource.isPartialSource ()); + if (bWithPayload) + { + try (final NonBlockingByteArrayOutputStream aBAOS = new NonBlockingByteArrayOutputStream (); + final Base64OutputStream aB64OS = new Base64OutputStream (aBAOS)) + { + aSource.writeTo (aB64OS); + aB64OS.flushBase64 (); + ret.add (JSON_PAYLOAD, aBAOS.getAsString (StandardCharsets.ISO_8859_1)); + } + catch (final IOException ex) + { + LOGGER.error ("Failed to write Base64 encoded payload to JSON", ex); + } + } + return ret; + } + /** * Create the VES status as a JSON Object.
* *
    * {
-   *   "lastModification" : dateTime
+   *   "lastModification" : dateTime,
    *   "type" : string,
    *   "validFrom" : string?,
    *   "validTo" : string?,
-   *   "deprecationReason" : string?
+   *   "deprecationReason" : string?,
    *   "replacementVesid" : string?
    * }
    * 
@@ -514,11 +572,11 @@ public static IJsonObject getJsonVESStatus (@Nonnull final IValidationExecutorSe * "name" : string, * "deprecated" : boolean, * "status" : { - * "lastModification" : dateTime + * "lastModification" : dateTime, * "type" : string, * "validFrom" : string?, * "validTo" : string?, - * "deprecationReason" : string? + * "deprecationReason" : string?, * "replacementVesid" : string? * } * } @@ -544,7 +602,7 @@ public static IJsonObject getJsonVES (@Nonnull final IValidationExecutorSet * Add one global error to the response. Afterwards no validation results * should be added. The layout of the response object is very similar to the * one created by - * {@link #applyValidationResultList(IJsonObject, IValidationExecutorSet, List, Locale, long, MutableInt, MutableInt)}. + * {@link #applyValidationResultList(IJsonObject, IValidationExecutorSet, ValidationResultList, Locale, long, MutableInt, MutableInt)}. *
* *
@@ -697,11 +755,13 @@ public static IReadableResource getAsValidationResource (@Nullable final String
   public static ValidationResultList getAsValidationResultList (@Nullable final IJsonObject aJson)
   {
     // By default we're only resolving in the enum
-    return getAsValidationResultList (aJson, EValidationType::getFromIDOrNull);
+    return getAsValidationResultList (aJson,
+                                      EValidationType::getFromIDOrNull,
+                                      PhiveResultHelper::createValidationSource);
   }
 
   /**
-   * Try to parse a JSON structure and convert it back to a
+   * Try to parse the default JSON structure and convert it back to a
    * {@link ValidationResultList}.
    *
    * @param aJson
@@ -709,22 +769,53 @@ public static ValidationResultList getAsValidationResultList (@Nullable final IJ
    * @param aValidationTypeResolver
    *        The validation type resolver to be used. May not be
    *        null.
+   * @param aValidationSourceRestorer
+   *        The function to restore {@link IValidationSource} objects. May not
+   *        be null.
    * @return null in case reverse operation fails.
    */
   @Nullable
   public static ValidationResultList getAsValidationResultList (@Nullable final IJsonObject aJson,
-                                                                @Nonnull final Function  aValidationTypeResolver)
+                                                                @Nonnull final Function  aValidationTypeResolver,
+                                                                @Nonnull final IValidationSourceRestorer aValidationSourceRestorer)
   {
     ValueEnforcer.notNull (aValidationTypeResolver, "ValidationTypeResolver");
+    ValueEnforcer.notNull (aValidationSourceRestorer, "ValidationSourceRestorer");
 
     if (aJson == null)
       return null;
 
+    final IValidationSource aValidationSource;
+    {
+      final IJsonObject aJsonVS = aJson.getAsObject (PhiveJsonHelper.JSON_VALIDATION_SOURCE);
+      if (aJsonVS != null)
+      {
+        final String sBase64EncodedPayload = aJsonVS.getAsString (PhiveJsonHelper.JSON_PAYLOAD);
+        final byte [] aPayloadBytes = Base64.safeDecode (sBase64EncodedPayload);
+        if (aPayloadBytes == null)
+        {
+          // Error in base64 decoding
+          LOGGER.warn ("Failed to Base64 decode the provided payload");
+          aValidationSource = null;
+        }
+        else
+        {
+          aValidationSource = aValidationSourceRestorer.restoreValidationSource (aJsonVS.getAsString (PhiveJsonHelper.JSON_SOURCE_TYPE_ID),
+                                                                                 aJsonVS.getAsString (PhiveJsonHelper.JSON_SYSTEM_ID),
+                                                                                 aJsonVS.getAsBoolean (PhiveJsonHelper.JSON_PARTIAL_SOURCE,
+                                                                                                       false),
+                                                                                 aPayloadBytes);
+        }
+      }
+      else
+        aValidationSource = null;
+    }
+
     final IJsonArray aResults = aJson.getAsArray (JSON_RESULTS);
     if (aResults == null)
       return null;
 
-    final ValidationResultList ret = new ValidationResultList ();
+    final ValidationResultList ret = new ValidationResultList (aValidationSource);
     for (final IJson aResult : aResults)
     {
       final IJsonObject aResultObj = aResult.getAsObject ();
diff --git a/phive-result/src/main/java/com/helger/phive/result/xml/PhiveXMLHelper.java b/phive-result/src/main/java/com/helger/phive/result/xml/PhiveXMLHelper.java
index 84cd230f..f774c490 100644
--- a/phive-result/src/main/java/com/helger/phive/result/xml/PhiveXMLHelper.java
+++ b/phive-result/src/main/java/com/helger/phive/result/xml/PhiveXMLHelper.java
@@ -16,8 +16,9 @@
  */
 package com.helger.phive.result.xml;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
-import java.util.List;
 import java.util.Locale;
 import java.util.function.Function;
 
@@ -31,6 +32,8 @@
 
 import com.helger.commons.ValueEnforcer;
 import com.helger.commons.annotation.Nonempty;
+import com.helger.commons.base64.Base64;
+import com.helger.commons.base64.Base64OutputStream;
 import com.helger.commons.datetime.PDTWebDateHelper;
 import com.helger.commons.error.IError;
 import com.helger.commons.error.SingleError;
@@ -39,6 +42,7 @@
 import com.helger.commons.error.list.ErrorList;
 import com.helger.commons.error.text.ConstantHasErrorText;
 import com.helger.commons.io.resource.IReadableResource;
+import com.helger.commons.io.stream.NonBlockingByteArrayOutputStream;
 import com.helger.commons.lang.StackTraceHelper;
 import com.helger.commons.location.ILocation;
 import com.helger.commons.location.SimpleLocation;
@@ -46,6 +50,7 @@
 import com.helger.commons.state.ETriState;
 import com.helger.commons.string.StringHelper;
 import com.helger.commons.string.StringParser;
+import com.helger.commons.typeconvert.TypeConverter;
 import com.helger.diver.api.coord.DVRCoordinate;
 import com.helger.phive.api.EValidationType;
 import com.helger.phive.api.IValidationType;
@@ -57,6 +62,7 @@
 import com.helger.phive.api.result.ValidationResultList;
 import com.helger.phive.api.source.IValidationSource;
 import com.helger.phive.api.validity.EExtendedValidity;
+import com.helger.phive.result.IValidationSourceRestorer;
 import com.helger.phive.result.PhiveResultHelper;
 import com.helger.phive.result.exception.PhiveRestoredException;
 import com.helger.schematron.svrl.SVRLResourceError;
@@ -67,7 +73,7 @@
 /**
  * A utility class to create a common XML representation of a PHIVE result. Use
  * {@link #applyGlobalError(IMicroElement, String, long)} or
- * {@link #applyValidationResultList(IMicroElement, IValidationExecutorSet, List, Locale, long, MutableInt, MutableInt)}
+ * {@link #applyValidationResultList(IMicroElement, IValidationExecutorSet, ValidationResultList, Locale, long, MutableInt, MutableInt)}
  * to add the result to an arbitrary {@link IMicroElement}.
  *
  * @author Philip Helger
@@ -90,6 +96,13 @@ public final class PhiveXMLHelper
   public static final String XML_EXCEPTION = "exception";
   public static final String XML_TEST = "test";
 
+  // Added in 10.1.0
+  public static final String XML_VALIDATION_SOURCE = "validationSource";
+  public static final String XML_SOURCE_TYPE_ID = "sourceTypeID";
+  public static final String XML_SYSTEM_ID = "systemID";
+  public static final String XML_PARTIAL_SOURCE = "partialSource";
+  public static final String XML_PAYLOAD = "payload";
+
   public static final String XML_VESID = "vesid";
   public static final String XML_NAME = "name";
   public static final String XML_DEPRECATED = "deprecated";
@@ -384,6 +397,58 @@ public static IError getAsIError (@Nonnull final IMicroElement aObj)
                             aLinkedException);
   }
 
+  /**
+   * Create the Validation Source details as an XML Object.
+ * + *
+   * <validationSource>
+   *   <implementation>string</implementation>
+   *   <systemID>string</systemID>?
+   *   <partialSource>boolean</partialSource>
+   *   <payload>base64Binary</payload>?
+   * </validationSource>
+   * 
+ * + * @param aSource + * The validation source to use. May not be null. + * @param bWithPayload + * true to include the payload, or false to + * omit it. + * @param sElementName + * The XML element name to use. May neither be null nor + * empty. + * @return The created XML object. + * @since 10.1.0 + */ + @Nonnull + public static IMicroElement getXMLValidationSource (@Nonnull final IValidationSource aSource, + final boolean bWithPayload, + @Nonnull @Nonempty final String sElementName) + { + ValueEnforcer.notNull (aSource, "Source"); + + final IMicroElement ret = new MicroElement (sElementName); + ret.appendElement (XML_SOURCE_TYPE_ID).appendText (aSource.getValidationSourceTypeID ()); + if (aSource.hasSystemID ()) + ret.appendElement (XML_SYSTEM_ID).appendText (aSource.getSystemID ()); + ret.appendElement (XML_PARTIAL_SOURCE).appendText (aSource.isPartialSource ()); + if (bWithPayload) + { + try (final NonBlockingByteArrayOutputStream aBAOS = new NonBlockingByteArrayOutputStream (); + final Base64OutputStream aB64OS = new Base64OutputStream (aBAOS)) + { + aSource.writeTo (aB64OS); + aB64OS.flushBase64 (); + ret.appendElement (XML_PAYLOAD).appendText (aBAOS.getAsString (StandardCharsets.ISO_8859_1)); + } + catch (final IOException ex) + { + LOGGER.error ("Failed to write Base64 encoded payload to XML", ex); + } + } + return ret; + } + /** * Create the VES status details as an XML Object.
* @@ -473,7 +538,7 @@ public static IMicroElement getXMLVES (@Nonnull final IValidationExecutorSet * Add one global error to the response. Afterwards no validation results * should be added. The layout of the response object is very similar to the * one created by - * {@link #applyValidationResultList(IMicroElement, IValidationExecutorSet, List, Locale, long, MutableInt, MutableInt)}. + * {@link #applyValidationResultList(IMicroElement, IValidationExecutorSet, ValidationResultList, Locale, long, MutableInt, MutableInt)}. *
* *
@@ -610,11 +675,13 @@ public static  IValidationExecutorSet  getAsVES
   public static ValidationResultList getAsValidationResultList (@Nullable final IMicroElement aXML)
   {
     // By default we're only resolving in the enum
-    return getAsValidationResultList (aXML, EValidationType::getFromIDOrNull);
+    return getAsValidationResultList (aXML,
+                                      EValidationType::getFromIDOrNull,
+                                      PhiveResultHelper::createValidationSource);
   }
 
   /**
-   * Try to parse a XML structure and convert it back to a
+   * Try to parse the default XML structure and convert it back to a
    * {@link ValidationResultList}.
    *
    * @param aXML
@@ -622,18 +689,51 @@ public static ValidationResultList getAsValidationResultList (@Nullable final IM
    * @param aValidationTypeResolver
    *        The validation type resolver to be used. May not be
    *        null.
+   * @param aValidationSourceRestorer
+   *        The function to restore {@link IValidationSource} objects. May not
+   *        be null.
    * @return null in case reverse operation fails.
    */
   @Nullable
   public static ValidationResultList getAsValidationResultList (@Nullable final IMicroElement aXML,
-                                                                @Nonnull final Function  aValidationTypeResolver)
+                                                                @Nonnull final Function  aValidationTypeResolver,
+                                                                @Nonnull final IValidationSourceRestorer aValidationSourceRestorer)
   {
     ValueEnforcer.notNull (aValidationTypeResolver, "ValidationTypeResolver");
 
     if (aXML == null)
       return null;
 
-    final ValidationResultList ret = new ValidationResultList ();
+    final IValidationSource aValidationSource;
+    {
+      final IMicroElement eVS = aXML.getFirstChildElement (XML_VALIDATION_SOURCE);
+      if (eVS != null)
+      {
+        final String sBase64EncodedPayload = MicroHelper.getChildTextContentTrimmed (eVS, XML_PAYLOAD);
+        final byte [] aPayloadBytes = Base64.safeDecode (sBase64EncodedPayload);
+        if (aPayloadBytes == null)
+        {
+          // Error in base64 decoding
+          LOGGER.warn ("Failed to Base64 decode the provided payload");
+          aValidationSource = null;
+        }
+        else
+        {
+          aValidationSource = aValidationSourceRestorer.restoreValidationSource (MicroHelper.getChildTextContentTrimmed (eVS,
+                                                                                                                         XML_SOURCE_TYPE_ID),
+                                                                                 MicroHelper.getChildTextContentTrimmed (eVS,
+                                                                                                                         XML_SYSTEM_ID),
+                                                                                 TypeConverter.convertToBoolean (MicroHelper.getChildTextContentTrimmed (eVS,
+                                                                                                                                                         XML_PARTIAL_SOURCE),
+                                                                                                                 false),
+                                                                                 aPayloadBytes);
+        }
+      }
+      else
+        aValidationSource = null;
+    }
+
+    final ValidationResultList ret = new ValidationResultList (aValidationSource);
     for (final IMicroElement eResult : aXML.getAllChildElements (XML_RESULT))
     {
       // Fall back to previous status
diff --git a/phive-result/src/main/java/com/helger/phive/result/xml/XMLValidationResultListHelper.java b/phive-result/src/main/java/com/helger/phive/result/xml/XMLValidationResultListHelper.java
index 11a15132..7959115c 100644
--- a/phive-result/src/main/java/com/helger/phive/result/xml/XMLValidationResultListHelper.java
+++ b/phive-result/src/main/java/com/helger/phive/result/xml/XMLValidationResultListHelper.java
@@ -38,6 +38,7 @@
 import com.helger.phive.api.executorset.IValidationExecutorSet;
 import com.helger.phive.api.result.ValidationResult;
 import com.helger.phive.api.result.ValidationResultList;
+import com.helger.phive.api.source.IValidationSource;
 import com.helger.phive.api.validity.EExtendedValidity;
 import com.helger.phive.result.PhiveResultHelper;
 import com.helger.xml.microdom.IMicroElement;
@@ -54,6 +55,9 @@ public class XMLValidationResultListHelper
 {
   private static final Logger LOGGER = LoggerFactory.getLogger (XMLValidationResultListHelper.class);
 
+  private Function  m_aSourceToXML = src -> PhiveXMLHelper.getXMLValidationSource (src,
+                                                                                                                     true,
+                                                                                                                     PhiveXMLHelper.XML_VALIDATION_SOURCE);
   private IValidationExecutorSet  m_aVES;
   private Function , IMicroElement> m_aVESToXML = ves -> PhiveXMLHelper.getXMLVES (ves,
                                                                                                               PhiveXMLHelper.XML_VES);
@@ -68,6 +72,13 @@ public class XMLValidationResultListHelper
   public XMLValidationResultListHelper ()
   {}
 
+  @Nonnull
+  public XMLValidationResultListHelper sourceToXML (@Nullable final Function  a)
+  {
+    m_aSourceToXML = a;
+    return this;
+  }
+
   @Nonnull
   public XMLValidationResultListHelper ves (@Nullable final IValidationExecutorSet  a)
   {
@@ -161,6 +172,14 @@ public void applyTo (@Nonnull final IMicroElement aResponse,
     ValueEnforcer.notNull (aDisplayLocale, "DisplayLocale");
     ValueEnforcer.isGE0 (nDurationMilliseconds, "DurationMilliseconds");
 
+    // Added in 10.1.0
+    if (aValidationResultList.hasValidationSource () && m_aSourceToXML != null)
+    {
+      final IMicroElement eSource = m_aSourceToXML.apply (aValidationResultList.getValidationSource ());
+      if (eSource != null)
+        aResponse.appendChild (eSource);
+    }
+
     if (m_aVES != null && m_aVESToXML != null)
     {
       final IMicroElement eVES = m_aVESToXML.apply (m_aVES);
diff --git a/phive-result/src/test/java/com/helger/phive/result/json/PhiveJsonHelperTest.java b/phive-result/src/test/java/com/helger/phive/result/json/PhiveJsonHelperTest.java
index a432cf7c..b0a1f8dc 100644
--- a/phive-result/src/test/java/com/helger/phive/result/json/PhiveJsonHelperTest.java
+++ b/phive-result/src/test/java/com/helger/phive/result/json/PhiveJsonHelperTest.java
@@ -40,6 +40,7 @@
 import com.helger.diver.api.version.DVRVersionException;
 import com.helger.json.IJsonObject;
 import com.helger.json.JsonObject;
+import com.helger.json.serialize.JsonWriterSettings;
 import com.helger.phive.api.execute.ValidationExecutionManager;
 import com.helger.phive.api.executorset.IValidationExecutorSet;
 import com.helger.phive.api.executorset.ValidationExecutorSet;
@@ -143,6 +144,8 @@ public void testValidationResultsBackAndForth () throws DVRVersionException
     final IJsonObject aObj2 = new JsonObject ();
     PhiveJsonHelper.applyValidationResultList (aObj2, aVES2, aVRL2, aDisplayLocale, 123, null, null);
 
+    assertEquals (aObj.getAsJsonString (JsonWriterSettings.DEFAULT_SETTINGS_FORMATTED),
+                  aObj2.getAsJsonString (JsonWriterSettings.DEFAULT_SETTINGS_FORMATTED));
     CommonsTestHelper.testDefaultImplementationWithEqualContentObject (aObj, aObj2);
   }
 
diff --git a/phive-xml/src/main/java/com/helger/phive/xml/source/IValidationSourceXML.java b/phive-xml/src/main/java/com/helger/phive/xml/source/IValidationSourceXML.java
index ff25e5ea..a7324e81 100644
--- a/phive-xml/src/main/java/com/helger/phive/xml/source/IValidationSourceXML.java
+++ b/phive-xml/src/main/java/com/helger/phive/xml/source/IValidationSourceXML.java
@@ -27,12 +27,14 @@
 import com.helger.xml.transform.TransformSourceFactory;
 
 /**
- * XML validation source.
+ * XML validation source interface.
  *
  * @author Philip Helger
  */
 public interface IValidationSourceXML extends IValidationSource
 {
+  String VALIDATION_SOURCE_TYPE = "xml";
+
   /**
    * @return The source node to be validated. This may either be the whole DOM
    *         Document or a single DOM Element. May be null.
diff --git a/phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXML.java b/phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXML.java
index 1d6db26f..45f1f618 100644
--- a/phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXML.java
+++ b/phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXML.java
@@ -16,20 +16,22 @@
  */
 package com.helger.phive.xml.source;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.util.function.Supplier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import javax.xml.transform.Source;
+import javax.annotation.WillNotClose;
 
 import org.w3c.dom.Node;
 
 import com.helger.commons.ValueEnforcer;
+import com.helger.commons.annotation.Nonempty;
 import com.helger.commons.io.resource.IReadableResource;
 import com.helger.commons.string.ToStringGenerator;
 import com.helger.xml.XMLHelper;
-import com.helger.xml.serialize.read.DOMReader;
-import com.helger.xml.transform.TransformSourceFactory;
+import com.helger.xml.serialize.write.XMLWriter;
 
 /**
  * Default implementation of {@link IValidationSourceXML}.
@@ -62,6 +64,13 @@ public ValidationSourceXML (@Nullable final String sSystemID,
     m_bPartialSource = bPartialSource;
   }
 
+  @Nonnull
+  @Nonempty
+  public String getValidationSourceTypeID ()
+  {
+    return VALIDATION_SOURCE_TYPE;
+  }
+
   @Nullable
   public String getSystemID ()
   {
@@ -85,6 +94,12 @@ public boolean isPartialSource ()
     return m_bPartialSource;
   }
 
+  public void writeTo (@Nonnull @WillNotClose final OutputStream aOS) throws IOException
+  {
+    if (XMLWriter.writeToStream (getNode (), aOS).isFailure ())
+      throw new IOException ("Failed write XML node to OutputStream");
+  }
+
   @Override
   public String toString ()
   {
@@ -138,16 +153,6 @@ public static ValidationSourceXML createPartial (@Nullable final String sSystemI
   @Nonnull
   public static ValidationSourceXML create (@Nonnull final IReadableResource aResource)
   {
-    // Read on demand only
-    return new ValidationSourceXML (aResource.getPath (), () -> DOMReader.readXMLDOM (aResource), false)
-    {
-      @Override
-      @Nonnull
-      public Source getAsTransformSource ()
-      {
-        // Use resource as TransformSource to get error line and column
-        return TransformSourceFactory.create (aResource);
-      }
-    };
+    return new ValidationSourceXMLReadableResource (aResource);
   }
 }
diff --git a/phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXMLReadableResource.java b/phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXMLReadableResource.java
new file mode 100644
index 00000000..0f6b55bc
--- /dev/null
+++ b/phive-xml/src/main/java/com/helger/phive/xml/source/ValidationSourceXMLReadableResource.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
+ * philip[at]helger[dot]com
+ *
+ * Licensed 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 com.helger.phive.xml.source;
+
+import javax.annotation.Nonnull;
+import javax.xml.transform.Source;
+
+import com.helger.commons.io.resource.IReadableResource;
+import com.helger.commons.string.ToStringGenerator;
+import com.helger.xml.serialize.read.DOMReader;
+import com.helger.xml.transform.TransformSourceFactory;
+
+/**
+ * Special ValidationSourceXML based on a complete {@link IReadableResource}.
+ *
+ * @author Philip Helger
+ * @since 10.1.0
+ */
+public class ValidationSourceXMLReadableResource extends ValidationSourceXML
+{
+  private final IReadableResource m_aResource;
+
+  public ValidationSourceXMLReadableResource (@Nonnull final IReadableResource aResource)
+  {
+    // Read on demand only
+    super (aResource.getPath (), () -> DOMReader.readXMLDOM (aResource), false);
+    m_aResource = aResource;
+  }
+
+  @Nonnull
+  public final IReadableResource getResource ()
+  {
+    return m_aResource;
+  }
+
+  @Override
+  @Nonnull
+  public Source getAsTransformSource ()
+  {
+    // Use resource as TransformSource to get error line and column
+    return TransformSourceFactory.create (m_aResource);
+  }
+
+  @Override
+  public String toString ()
+  {
+    return ToStringGenerator.getDerived (super.toString ()).append ("Resource", m_aResource).getToString ();
+  }
+}