11/*
2- * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below.
2+ * Copyright (c) 2019-2023 Tada AB and other contributors, as listed below.
33 *
44 * All rights reserved. This program and the accompanying materials
55 * are made available under the terms of the The BSD 3-Clause License
1414import java .io .Reader ;
1515import java .sql .SQLException ;
1616import java .sql .SQLXML ;
17+ import java .util .List ;
18+ import static java .util .Objects .requireNonNull ;
19+ import java .util .function .Consumer ;
1720import javax .xml .stream .XMLInputFactory ; // for javadoc
1821import javax .xml .stream .XMLResolver ; // for javadoc
1922import javax .xml .stream .XMLStreamReader ;
@@ -126,16 +129,139 @@ public static final class XML
126129 {
127130 private XML () { } // no instances
128131
132+ /**
133+ * Attempts a given action (typically to set something) using a given
134+ * value, trying one or more supplied keys in order until the action
135+ * succeeds with no exception.
136+ *<p>
137+ * This logic is common to the
138+ * {@link Parsing#setFirstSupportedFeature setFirstSupportedFeature}
139+ * and
140+ * {@link Parsing#setFirstSupportedProperty setFirstSupportedProperty}
141+ * methods, and is exposed here because it may be useful for other
142+ * tasks in Java's XML APIs, such as configuring {@code Transformer}s.
143+ *<p>
144+ * If any attempt succeeds, null is returned. If no attempt
145+ * succeeds, the first exception caught is returned, with any
146+ * exceptions from the subsequent attempts retrievable from it with
147+ * {@link Exception#getSuppressed getSuppressed}. The return is
148+ * immediate, without any remaining names being tried, if an exception
149+ * is caught that is not assignable to a class in the
150+ * <var>expected</var> list. Such an exception will be passed to the
151+ * <var>onUnexpected</var> handler if that is non-null; otherwise,
152+ * it will be returned (or added to the suppressed list of the
153+ * exception to be returned) just as expected exceptions are.
154+ * @param setter typically a method reference for a method that
155+ * takes a string key and some value.
156+ * @param value the value to pass to the setter
157+ * @param expected a list of exception classes that can be foreseen
158+ * to indicate that a key was not recognized, and the operation
159+ * should be retried with the next possible key.
160+ * @param onUnexpected invoked, if non-null, on an {@code Exception}
161+ * that is caught and matches nothing in the expected list, instead
162+ * of returning it. If this parameter is null, such an exception is
163+ * returned (or added to the suppressed list of the exception to be
164+ * returned), just as for expected exceptions, but the return is
165+ * immediate, without trying remaining names, if any.
166+ * @param names one or more String keys to be tried in order until
167+ * the action succeeds.
168+ * @return null if any attempt succeeded, otherwise an exception,
169+ * which may have further exceptions in its suppressed list.
170+ */
171+ public static <T , V extends T > Exception setFirstSupported (
172+ SetMethod <? super T > setter , V value ,
173+ List <Class <? extends Exception >> expected ,
174+ Consumer <? super Exception > onUnexpected , String ... names )
175+ {
176+ requireNonNull (expected );
177+ Exception caught = null ;
178+ for ( String name : names )
179+ {
180+ try
181+ {
182+ setter .set (name , value );
183+ return null ;
184+ }
185+ catch ( Exception e )
186+ {
187+ boolean benign =
188+ expected .stream ().anyMatch (c -> c .isInstance (e ));
189+
190+ if ( benign || null == onUnexpected )
191+ {
192+ if ( null == caught )
193+ caught = e ;
194+ else
195+ caught .addSuppressed (e );
196+ }
197+ else
198+ onUnexpected .accept (e );
199+
200+ if ( ! benign )
201+ break ;
202+ }
203+ }
204+ return caught ;
205+ }
206+
207+ /**
208+ * A functional interface fitting various {@code setFeature} or
209+ * {@code setProperty} methods in Java XML APIs.
210+ *<p>
211+ * The XML APIs have a number of methods on various interfaces that can
212+ * be used to set some property or feature, and can generally be
213+ * assigned to this functional interface by bound method reference, and
214+ * used with {@link #setFirstSupported setFirstSupported}.
215+ */
216+ @ FunctionalInterface
217+ public interface SetMethod <T >
218+ {
219+ void set (String key , T value ) throws Exception ;
220+ }
221+
129222 /**
130223 * Interface with methods to adjust the restrictions on XML parsing
131224 * that are commonly considered when XML content might be from untrusted
132225 * sources.
133226 *<p>
134- * The adjusting methods are best-effort and do not provide an
135- * indication of whether the requested adjustment was made. Not all of
227+ * The adjusting methods are best-effort; not all of
136228 * the adjustments are available for all flavors of {@code Source} or
137229 * {@code Result} or for all parser implementations or versions the Java
138- * runtime may supply.
230+ * runtime may supply. Cases where a requested adjustment has not been
231+ * made are handled as follows:
232+ *<p>
233+ * Any sequence of adjustment calls will ultimately be followed by a
234+ * {@code get}. During the sequence of adjustments, exceptions caught
235+ * are added to a signaling list or to a quiet list, where "added to"
236+ * means that if either list has a first exception, any caught later are
237+ * attached to that exception with
238+ * {@link Exception#addSuppressed addSuppressed}.
239+ *<p>
240+ * For each adjustment (and depending on the type of underlying
241+ * {@code Source} or {@code Result}), one or more exception types will
242+ * be 'expected' as indications that an identifying key or value for
243+ * that adjustment was not recognized. This implementation may continue
244+ * trying to apply the adjustment, using other keys that have at times
245+ * been used to identify it. Expected exceptions caught during these
246+ * attempts form a temporary list (a first exception and those attached
247+ * to it by {@code addSuppressed}). Once any such attempt succeeds, the
248+ * adjustment is considered made, and any temporary expected exceptions
249+ * list from the adjustment is discarded. If no attempt succeeded, the
250+ * temporary list is retained, by adding its head exception to the quiet
251+ * list.
252+ *<p>
253+ * Any exceptions caught that are not instances of any of the 'expected'
254+ * types are added to the signaling list.
255+ *<p>
256+ * When {@code get} is called, the head exception on the signaling list,
257+ * if any, is thrown. Otherwise, the head exception on the quiet list,
258+ * if any, is logged at [@code WARNING} level.
259+ *<p>
260+ * During a chain of adjustments, {@link #lax lax()} can be called to
261+ * tailor the handling of the quiet list. A {@code lax()} call applies
262+ * to whatever exceptions have been added to the quiet list up to that
263+ * point. To discard them, call {@code lax(true)}; to move them to the
264+ * signaling list, call {@code lax(false)}.
139265 */
140266 public interface Parsing <T extends Parsing <T >>
141267 {
@@ -173,14 +299,14 @@ public interface Parsing<T extends Parsing<T>>
173299
174300 /**
175301 * For a feature that may have been identified by more than one URI
176- * in different parsers or versions, try passing the supplied
302+ * in different parsers or versions, tries passing the supplied
177303 * <em>value</em> with each URI from <em>names</em> in order until
178304 * one is not rejected by the underlying parser.
179305 */
180306 T setFirstSupportedFeature (boolean value , String ... names );
181307
182308 /**
183- * Make a best effort to apply the recommended, restrictive
309+ * Makes a best effort to apply the recommended, restrictive
184310 * defaults from the OWASP cheat sheet, to the extent they are
185311 * supported by the underlying parser, runtime, and version.
186312 *<p>
@@ -196,7 +322,7 @@ public interface Parsing<T extends Parsing<T>>
196322 /**
197323 * For a parser property (in DOM parlance, attribute) that may have
198324 * been identified by more than one URI in different parsers or
199- * versions, try passing the supplied <em>value</em> with each URI
325+ * versions, tries passing the supplied <em>value</em> with each URI
200326 * from <em>names</em> in order until one is not rejected by the
201327 * underlying parser.
202328 *<p>
@@ -278,7 +404,7 @@ public interface Parsing<T extends Parsing<T>>
278404 T accessExternalSchema (String protocols );
279405
280406 /**
281- * Set an {@link EntityResolver} of the type used by SAX and DOM
407+ * Sets an {@link EntityResolver} of the type used by SAX and DOM
282408 * <em>(optional operation)</em>.
283409 *<p>
284410 * This method only succeeds for a {@code SAXSource} or
@@ -297,7 +423,7 @@ public interface Parsing<T extends Parsing<T>>
297423 T entityResolver (EntityResolver resolver );
298424
299425 /**
300- * Set a {@link Schema} to be applied during SAX or DOM parsing
426+ * Sets a {@link Schema} to be applied during SAX or DOM parsing
301427 *<em>(optional operation)</em>.
302428 *<p>
303429 * This method only succeeds for a {@code SAXSource} or
@@ -316,6 +442,31 @@ public interface Parsing<T extends Parsing<T>>
316442 * already.
317443 */
318444 T schema (Schema schema );
445+
446+ /**
447+ * Tailors the treatment of 'quiet' exceptions during a chain of
448+ * best-effort adjustments.
449+ *<p>
450+ * See {@link Parsing the class description} for an explanation of
451+ * the signaling and quiet lists.
452+ *<p>
453+ * This method applies to whatever exceptions may have been added to
454+ * the quiet list by best-effort adjustments made up to that point.
455+ * They can be moved to the signaling list with {@code lax(false)},
456+ * or simply discarded with {@code lax(true)}. In either case, the
457+ * quiet list is left empty when {@code lax} returns.
458+ *<p>
459+ * At the time a {@code get} method is later called, any exception
460+ * at the head of the signaling list will be thrown (possibly
461+ * wrapped in an exception permitted by {@code get}'s {@code throws}
462+ * clause), with any later exceptions on that list retrievable from
463+ * the head exception with
464+ * {@link Exception#getSuppressed getSuppressed}. Otherwise, any
465+ * exception at the head of the quiet list (again with any later
466+ * ones attached as its suppressed list) will be logged at
467+ * {@code WARNING} level.
468+ */
469+ T lax (boolean discard );
319470 }
320471
321472 /**
@@ -347,12 +498,17 @@ public interface Source<T extends javax.xml.transform.Source>
347498 extends Parsing <Source <T >>, javax .xml .transform .Source
348499 {
349500 /**
350- * Return an object of the expected {@code Source} subtype
501+ * Returns an object of the expected {@code Source} subtype
351502 * reflecting any adjustments made with the other methods.
503+ *<p>
504+ * Refer to {@link Parsing the {@code Parsing} class description}
505+ * and the {@link Parsing#lax lax()} method for how any exceptions
506+ * caught while applying best-effort adjustments are handled.
352507 * @return an implementing object of the expected Source subtype
353508 * @throws SQLException for any reason that {@code getSource} might
354509 * have thrown when supplying the corresponding non-Adjusting
355- * subtype of Source.
510+ * subtype of Source, or for reasons saved while applying
511+ * adjustments.
356512 */
357513 T get () throws SQLException ;
358514 }
@@ -392,12 +548,16 @@ public interface Result<T extends javax.xml.transform.Result>
392548 extends Parsing <Result <T >>, javax .xml .transform .Result
393549 {
394550 /**
395- * Return an object of the expected {@code Result} subtype
551+ * Returns an object of the expected {@code Result} subtype
396552 * reflecting any adjustments made with the other methods.
553+ * Refer to {@link Parsing the {@code Parsing} class description}
554+ * and the {@link Parsing#lax lax()} method for how any exceptions
555+ * caught while applying best-effort adjustments are handled.
397556 * @return an implementing object of the expected Result subtype
398557 * @throws SQLException for any reason that {@code getResult} might
399558 * have thrown when supplying the corresponding non-Adjusting
400- * subtype of Result.
559+ * subtype of Result, or for reasons saved while applying
560+ * adjustments.
401561 */
402562 T get () throws SQLException ;
403563 }
@@ -428,7 +588,7 @@ public interface Result<T extends javax.xml.transform.Result>
428588 public interface SourceResult extends Result <SourceResult >
429589 {
430590 /**
431- * Supply the {@code Source} instance that is the source of the
591+ * Supplies the {@code Source} instance that is the source of the
432592 * content.
433593 *<p>
434594 * This method must be called before any of the inherited adjustment
@@ -484,7 +644,8 @@ SourceResult set(javax.xml.transform.stax.StAXSource source)
484644 throws SQLException ;
485645
486646 /**
487- * Provide the content to be copied in the form of a {@code String}.
647+ * Provides the content to be copied in the form of a
648+ * {@code String}.
488649 *<p>
489650 * An exception from the pattern of {@code Source}-typed arguments,
490651 * this method simplifies retrofitting adjustments into code that
@@ -507,11 +668,14 @@ SourceResult set(javax.xml.transform.dom.DOMSource source)
507668 throws SQLException ;
508669
509670 /**
510- * Return the result {@code SQLXML} instance ready for handing off
671+ * Returns the result {@code SQLXML} instance ready for handing off
511672 * to PostgreSQL.
512673 *<p>
513- * This method must be called after any of the inherited adjustment
514- * methods.
674+ * The handling/logging of exceptions normally handled in a
675+ * {@code get} method happens here for a {@code SourceResult}.
676+ *<p>
677+ * Any necessary calls of the inherited adjustment methods must be
678+ * made before this method is called.
515679 */
516680 SQLXML getSQLXML () throws SQLException ;
517681 }
0 commit comments