diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml
index c534680a1..fb456a7fa 100644
--- a/.github/workflows/ci-runnerpg.yml
+++ b/.github/workflows/ci-runnerpg.yml
@@ -215,6 +215,8 @@ jobs:
import static org.postgresql.pljava.packaging.Node.stateMachine;
import static org.postgresql.pljava.packaging.Node.isVoidResultSet;
import static org.postgresql.pljava.packaging.Node.s_isWindows;
+ import static
+ org.postgresql.pljava.packaging.Node.NOTHING_OR_PGJDBC_ZERO_COUNT;
/*
* Imports that will be needed to serve a jar file over http
* when the time comes for testing that.
@@ -339,7 +341,9 @@ jobs:
// state 1: consume any diagnostics, or to state 2 with same item
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
- // state 2: must be end of input
+ NOTHING_OR_PGJDBC_ZERO_COUNT, // state 2
+
+ // state 3: must be end of input
(o,p,q) -> null == o
);
}
@@ -566,6 +570,9 @@ jobs:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
@@ -598,6 +605,8 @@ jobs:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
}
@@ -633,6 +642,7 @@ jobs:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
@@ -647,6 +657,7 @@ jobs:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
}
@@ -672,6 +683,7 @@ jobs:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
@@ -687,6 +699,8 @@ jobs:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
}
diff --git a/appveyor.yml b/appveyor.yml
index a0e534ee2..ed964f202 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -108,6 +108,8 @@ test_script:
import static org.postgresql.pljava.packaging.Node.q;
import static org.postgresql.pljava.packaging.Node.stateMachine;
import static org.postgresql.pljava.packaging.Node.isVoidResultSet;
+ import static
+ org.postgresql.pljava.packaging.Node.NOTHING_OR_PGJDBC_ZERO_COUNT;
/*
* Imports that will be needed to serve a jar file over http
* when the time comes for testing that.
@@ -221,7 +223,9 @@ test_script:
// state 1: consume any diagnostics, or show same item to state 2
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
- // state 2: must be end of input
+ NOTHING_OR_PGJDBC_ZERO_COUNT, // state 2
+
+ // state 3: must be end of input
(o,p,q) -> null == o
);
}
@@ -448,6 +452,9 @@ test_script:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
@@ -480,6 +487,8 @@ test_script:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
}
@@ -515,6 +524,7 @@ test_script:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
@@ -529,6 +539,7 @@ test_script:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
}
@@ -554,6 +565,7 @@ test_script:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
@@ -569,6 +581,8 @@ test_script:
.peek(Node::peek),
(o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
+ NOTHING_OR_PGJDBC_ZERO_COUNT,
(o,p,q) -> null == o
);
}
diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java b/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java
index 9a2d379b7..3c8ce6398 100644
--- a/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java
+++ b/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below.
+ * Copyright (c) 2019-2023 Tada AB and other contributors, as listed below.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the The BSD 3-Clause License
@@ -14,6 +14,9 @@
import java.io.Reader;
import java.sql.SQLException;
import java.sql.SQLXML;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.function.Consumer;
import javax.xml.stream.XMLInputFactory; // for javadoc
import javax.xml.stream.XMLResolver; // for javadoc
import javax.xml.stream.XMLStreamReader;
@@ -126,16 +129,139 @@ public static final class XML
{
private XML() { } // no instances
+ /**
+ * Attempts a given action (typically to set something) using a given
+ * value, trying one or more supplied keys in order until the action
+ * succeeds with no exception.
+ *
+ * This logic is common to the
+ * {@link Parsing#setFirstSupportedFeature setFirstSupportedFeature}
+ * and
+ * {@link Parsing#setFirstSupportedProperty setFirstSupportedProperty}
+ * methods, and is exposed here because it may be useful for other
+ * tasks in Java's XML APIs, such as configuring {@code Transformer}s.
+ *
+ * If any attempt succeeds, null is returned. If no attempt
+ * succeeds, the first exception caught is returned, with any
+ * exceptions from the subsequent attempts retrievable from it with
+ * {@link Exception#getSuppressed getSuppressed}. The return is
+ * immediate, without any remaining names being tried, if an exception
+ * is caught that is not assignable to a class in the
+ * expected list. Such an exception will be passed to the
+ * onUnexpected handler if that is non-null; otherwise,
+ * it will be returned (or added to the suppressed list of the
+ * exception to be returned) just as expected exceptions are.
+ * @param setter typically a method reference for a method that
+ * takes a string key and some value.
+ * @param value the value to pass to the setter
+ * @param expected a list of exception classes that can be foreseen
+ * to indicate that a key was not recognized, and the operation
+ * should be retried with the next possible key.
+ * @param onUnexpected invoked, if non-null, on an {@code Exception}
+ * that is caught and matches nothing in the expected list, instead
+ * of returning it. If this parameter is null, such an exception is
+ * returned (or added to the suppressed list of the exception to be
+ * returned), just as for expected exceptions, but the return is
+ * immediate, without trying remaining names, if any.
+ * @param names one or more String keys to be tried in order until
+ * the action succeeds.
+ * @return null if any attempt succeeded, otherwise an exception,
+ * which may have further exceptions in its suppressed list.
+ */
+ public static Exception setFirstSupported(
+ SetMethod super T> setter, V value,
+ List> expected,
+ Consumer super Exception> onUnexpected, String... names)
+ {
+ requireNonNull(expected);
+ Exception caught = null;
+ for ( String name : names )
+ {
+ try
+ {
+ setter.set(name, value);
+ return null;
+ }
+ catch ( Exception e )
+ {
+ boolean benign =
+ expected.stream().anyMatch(c -> c.isInstance(e));
+
+ if ( benign || null == onUnexpected )
+ {
+ if ( null == caught )
+ caught = e;
+ else
+ caught.addSuppressed(e);
+ }
+ else
+ onUnexpected.accept(e);
+
+ if ( ! benign )
+ break;
+ }
+ }
+ return caught;
+ }
+
+ /**
+ * A functional interface fitting various {@code setFeature} or
+ * {@code setProperty} methods in Java XML APIs.
+ *
+ * The XML APIs have a number of methods on various interfaces that can
+ * be used to set some property or feature, and can generally be
+ * assigned to this functional interface by bound method reference, and
+ * used with {@link #setFirstSupported setFirstSupported}.
+ */
+ @FunctionalInterface
+ public interface SetMethod
+ {
+ void set(String key, T value) throws Exception;
+ }
+
/**
* Interface with methods to adjust the restrictions on XML parsing
* that are commonly considered when XML content might be from untrusted
* sources.
*
- * The adjusting methods are best-effort and do not provide an
- * indication of whether the requested adjustment was made. Not all of
+ * The adjusting methods are best-effort; not all of
* the adjustments are available for all flavors of {@code Source} or
* {@code Result} or for all parser implementations or versions the Java
- * runtime may supply.
+ * runtime may supply. Cases where a requested adjustment has not been
+ * made are handled as follows:
+ *
+ * Any sequence of adjustment calls will ultimately be followed by a
+ * {@code get}. During the sequence of adjustments, exceptions caught
+ * are added to a signaling list or to a quiet list, where "added to"
+ * means that if either list has a first exception, any caught later are
+ * attached to that exception with
+ * {@link Exception#addSuppressed addSuppressed}.
+ *
+ * For each adjustment (and depending on the type of underlying
+ * {@code Source} or {@code Result}), one or more exception types will
+ * be 'expected' as indications that an identifying key or value for
+ * that adjustment was not recognized. This implementation may continue
+ * trying to apply the adjustment, using other keys that have at times
+ * been used to identify it. Expected exceptions caught during these
+ * attempts form a temporary list (a first exception and those attached
+ * to it by {@code addSuppressed}). Once any such attempt succeeds, the
+ * adjustment is considered made, and any temporary expected exceptions
+ * list from the adjustment is discarded. If no attempt succeeded, the
+ * temporary list is retained, by adding its head exception to the quiet
+ * list.
+ *
+ * Any exceptions caught that are not instances of any of the 'expected'
+ * types are added to the signaling list.
+ *
+ * When {@code get} is called, the head exception on the signaling list,
+ * if any, is thrown. Otherwise, the head exception on the quiet list,
+ * if any, is logged at {@code WARNING} level.
+ *
+ * During a chain of adjustments, {@link #lax lax()} can be called to
+ * tailor the handling of the quiet list. A {@code lax()} call applies
+ * to whatever exceptions have been added to the quiet list up to that
+ * point. To discard them, call {@code lax(true)}; to move them to the
+ * signaling list, call {@code lax(false)}.
*/
public interface Parsing>
{
@@ -173,14 +299,14 @@ public interface Parsing>
/**
* For a feature that may have been identified by more than one URI
- * in different parsers or versions, try passing the supplied
+ * in different parsers or versions, tries passing the supplied
* value with each URI from names in order until
* one is not rejected by the underlying parser.
*/
T setFirstSupportedFeature(boolean value, String... names);
/**
- * Make a best effort to apply the recommended, restrictive
+ * Makes a best effort to apply the recommended, restrictive
* defaults from the OWASP cheat sheet, to the extent they are
* supported by the underlying parser, runtime, and version.
*
@@ -196,7 +322,7 @@ public interface Parsing>
/**
* For a parser property (in DOM parlance, attribute) that may have
* been identified by more than one URI in different parsers or
- * versions, try passing the supplied value with each URI
+ * versions, tries passing the supplied value with each URI
* from names in order until one is not rejected by the
* underlying parser.
*
@@ -278,7 +404,7 @@ public interface Parsing>
T accessExternalSchema(String protocols);
/**
- * Set an {@link EntityResolver} of the type used by SAX and DOM
+ * Sets an {@link EntityResolver} of the type used by SAX and DOM
* (optional operation).
*
* This method only succeeds for a {@code SAXSource} or
@@ -297,7 +423,7 @@ public interface Parsing>
T entityResolver(EntityResolver resolver);
/**
- * Set a {@link Schema} to be applied during SAX or DOM parsing
+ * Sets a {@link Schema} to be applied during SAX or DOM parsing
*(optional operation).
*
* This method only succeeds for a {@code SAXSource} or
@@ -316,6 +442,31 @@ public interface Parsing>
* already.
*/
T schema(Schema schema);
+
+ /**
+ * Tailors the treatment of 'quiet' exceptions during a chain of
+ * best-effort adjustments.
+ *
+ * See {@link Parsing the class description} for an explanation of
+ * the signaling and quiet lists.
+ *
+ * This method applies to whatever exceptions may have been added to
+ * the quiet list by best-effort adjustments made up to that point.
+ * They can be moved to the signaling list with {@code lax(false)},
+ * or simply discarded with {@code lax(true)}. In either case, the
+ * quiet list is left empty when {@code lax} returns.
+ *
+ * At the time a {@code get} method is later called, any exception
+ * at the head of the signaling list will be thrown (possibly
+ * wrapped in an exception permitted by {@code get}'s {@code throws}
+ * clause), with any later exceptions on that list retrievable from
+ * the head exception with
+ * {@link Exception#getSuppressed getSuppressed}. Otherwise, any
+ * exception at the head of the quiet list (again with any later
+ * ones attached as its suppressed list) will be logged at
+ * {@code WARNING} level.
+ */
+ T lax(boolean discard);
}
/**
@@ -347,12 +498,17 @@ public interface Source
extends Parsing