001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.serializer;
018
019import static java.util.stream.Collectors.*;
020import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.Shorts.*;
023
024import java.util.*;
025import java.util.concurrent.*;
026import java.util.function.*;
027import java.util.stream.*;
028
029import org.apache.juneau.commons.http.*;
030import org.apache.juneau.commons.inject.*;
031import org.apache.juneau.marshall.*;
032
033/**
034 * Represents a group of {@link Serializer Serializers} that can be looked up by media type.
035 *
036 * <h5 class='topic'>Description</h5>
037 *
038 * Provides the following features:
039 * <ul class='spaced-list'>
040 *    <li>
041 *       Finds serializers based on HTTP <c>Accept</c> header values.
042 *    <li>
043 *       Sets common properties on all serializers in a single method call.
044 *    <li>
045 *       Clones existing groups and all serializers within the group in a single method call.
046 * </ul>
047 *
048 * <h5 class='topic'>Match ordering</h5>
049 *
050 * Serializers are matched against <c>Accept</c> strings in the order they exist in this group.
051 *
052 * <p>
053 * Adding new entries will cause the entries to be prepended to the group.
054 * This allows for previous serializers to be overridden through subsequent calls.
055 *
056 * <p>
057 * For example, calling <code>g.append(S1.<jk>class</jk>,S2.<jk>class</jk>).append(S3.<jk>class</jk>,S4.<jk>class</jk>)</code>
058 * will result in the order <c>S3, S4, S1, S2</c>.
059 *
060 * <h5 class='section'>Example:</h5>
061 * <p class='bjava'>
062 *    <jc>// Construct a new serializer group</jc>
063 *    SerializerSet <jv>serializers</jv> = SerializerSet.<jsm>create</jsm>();
064 *       .add(JsonSerializer.<jk>class</jk>, UrlEncodingSerializer.<jk>class</jk>) <jc>// Add some serializers to it</jc>
065 *       .forEach(<jv>x</jv> -&gt; <jv>x</jv>.calendarFormat(<jsf>ISO_LOCAL_DATE_TIME</jsf>))
066 *       .forEachWS(<jv>x</jv> -&gt; <jv>x</jv>.ws())
067 *       .build();
068 *
069 *    <jc>// Find the appropriate serializer by Accept type</jc>
070 *    WriterSerializer <jv>serializer</jv> = <jv>serializers</jv>
071 *       .getWriterSerializer(<js>"text/foo, text/json;q=0.8, text/*;q:0.6, *\/*;q=0.0"</js>)
072 *       .orElseThrow();  <jc>// Throws if no serializer matched the Accept header.</jc>
073 *
074 *    <jc>// Serialize a bean to JSON text </jc>
075 *    AddressBook <jv>addressBook</jv> = <jk>new</jk> AddressBook();  <jc>// Bean to serialize.</jc>
076 *    String <jv>json</jv> = <jv>serializer</jv>.write(<jv>addressBook</jv>);
077 * </p>
078 *
079 * <h5 class='section'>Notes:</h5><ul>
080 *    <li class='note'>This class is thread safe and reusable.
081 * </ul>
082 *
083 * <h5 class='section'>See Also:</h5><ul>
084 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SerializersAndParsers">Serializers and Parsers</a>
085
086 * </ul>
087 */
088@SuppressWarnings({
089   "rawtypes", // Raw Consumer type required for generic builder dispatch across parameterized builder hierarchy.
090   "unchecked" // Builder forEach casts raw (Consumer)/(Class) actions to the builder type selected by the class argument
091})
092public class SerializerSet {
093
094   /**
095    * Builder class.
096    */
097   @SuppressWarnings({
098      "java:S115" // Constants use UPPER_snakeCase convention (e.g., CLASS_Inherit)
099   })
100   public static class Builder implements BeanStoreOverridable<Builder> {
101
102      private static final String CLASS_Inherit = "Inherit";
103
104      private BeanStore beanStore;
105      private SerializerSet impl;
106
107      List<Object> entries;
108      private MarshallingContext.Builder bcBuilder;
109
110      /**
111       * Create an empty serializer group builder.
112       *
113       * @param beanStore The bean store to use for creating beans.
114       */
115      protected Builder(BeanStore beanStore) {
116         this.beanStore = beanStore;
117         this.entries = list();
118      }
119
120      /**
121       * Clone an existing serializer group builder.
122       *
123       * <p>
124       * Serializer builders will be cloned during this process.
125       *
126       * @param copyFrom The serializer group that we're copying settings and serializers from.
127       *    <br>Cannot be <jk>null</jk>.
128       */
129      protected Builder(Builder copyFrom) {
130         this.beanStore = copyFrom.beanStore;
131         this.impl = copyFrom.impl;
132         bcBuilder = MarshallingContext.Builder.copyOrNull(copyFrom.bcBuilder);
133         entries = list();
134         copyFrom.entries.stream().map(this::copyBuilder).forEach(entries::add);
135      }
136
137      /**
138       * Clone an existing serializer group.
139       *
140       * @param copyFrom The serializer group that we're copying settings and serializers from.
141       *    <br>Cannot be <jk>null</jk>.
142       */
143      protected Builder(SerializerSet copyFrom) {
144         this.beanStore = BasicBeanStore.INSTANCE;
145         this.entries = list((Object[])copyFrom.entries);
146      }
147
148      /**
149       * Returns the bean store used by this builder.
150       *
151       * @return The bean store used by this builder.
152       */
153      public BeanStore beanStore() {
154         return beanStore;
155      }
156
157      /**
158       * Installs the supplied {@link BeanStore} as the {@code overridingParent} of this builder's bean store.
159       *
160       * <p>
161       * Wraps the current {@link #beanStore()} in a fresh {@link BasicBeanStore} whose {@code overridingParent}
162       * is the supplied store, so test-time overrides win over the builder's regular bean lookups during
163       * construction-time reflective injection.  Passing {@code null} is a no-op.
164       *
165       * @param store The override layer.  Can be <jk>null</jk> (no-op).
166       * @return This object.
167       * @since 10.0.0
168       */
169      @Override
170      @SuppressWarnings({
171         "resource" // BasicBeanStore lifecycle is owned by the enclosing builder; closed externally when the builder is disposed.
172      })
173      public Builder overridingBeanStore(BeanStore store) {
174         if (store != null)
175            this.beanStore = new BasicBeanStore(this.beanStore, store);
176         return this;
177      }
178
179      /**
180       * Adds the specified serializers to this group.
181       *
182       * <p>
183       * Entries are added in-order to the beginning of the list in the group.
184       *
185       * <p>
186       * The {@link NoInherit} class can be used to clear out the existing list of serializers before adding the new entries.
187       *
188       * <h5 class='section'>Example:</h5>
189       * <p class='bjava'>
190       *    SerializerSet.Builder <jv>builder</jv> = SerializerSet.<jsm>create</jsm>();  <jc>// Create an empty builder.</jc>
191       *
192       *    <jv>builder</jv>.add(FooSerializer.<jk>class</jk>);  <jc>// Now contains:  [FooSerializer]</jc>
193       *
194       *    <jv>builder</jv>.add(BarSerializer.<jk>class</jk>, BazSerializer.<jk>class</jk>);  <jc>// Now contains:  [BarParser,BazSerializer,FooSerializer]</jc>
195       *
196       *    <jv>builder</jv>.add(NoInherit.<jk>class</jk>, QuxSerializer.<jk>class</jk>);  <jc>// Now contains:  [QuxSerializer]</jc>
197       * </p>
198       *
199       * @param values The serializers to add to this group.
200       * @return This object.
201       * @throws IllegalArgumentException If one or more values do not extend from {@link Serializer}.
202       */
203      public Builder add(Class<?>...values) {
204         List<Object> l = list();
205         for (var e : values) {
206            if (Serializer.class.isAssignableFrom(e)) {
207               l.add(createBuilder(e));
208            } else {
209               throw rex("Invalid type passed to SerializeGroup.Builder.add(): %s", cn(e));
210            }
211         }
212         entries.addAll(0, l);
213         return this;
214      }
215
216      /**
217       * Registers the specified serializers with this group.
218       *
219       * <p>
220       * When passing in pre-instantiated serializers to this group, applying properties and transforms to the group
221       * do not affect them.
222       *
223       * @param s The serializers to append to this group.
224       * @return This object.
225       */
226      public Builder add(Serializer...s) {
227         prependAll(entries, (Object[])s);
228         return this;
229      }
230
231      /**
232       * Applies the specified annotations to all applicable serializer builders in this group.
233       *
234       * @param work The annotations to apply.
235       * @return This object.
236       */
237      public Builder apply(AnnotationWorkList work) {
238         return forEach(x -> x.apply(work));
239      }
240
241      /**
242       * Associates an existing bean context builder with all serializer builders in this group.
243       *
244       * @param value The bean context builder to associate.
245       *    <br>Can be <jk>null</jk> (clears any previously-associated bean context builder).
246       * @return This object.
247       */
248      public Builder marshallingContext(MarshallingContext.Builder value) {
249         bcBuilder = value;
250         forEach(x -> x.marshallingContext(value));
251         return this;
252      }
253
254      /**
255       * Applies an operation to the bean context builder.
256       *
257       * @param operation The operation to apply.
258       * @return This object.
259       */
260      public final Builder marshallingContext(Consumer<MarshallingContext.Builder> operation) {
261         if (nn(bcBuilder))
262            operation.accept(bcBuilder);
263         return this;
264      }
265
266      /**
267       * Returns <jk>true</jk> if at least one of the specified annotations can be applied to at least one serializer builder in this group.
268       *
269       * @param work The work to check.
270       * @return <jk>true</jk> if at least one of the specified annotations can be applied to at least one serializer builder in this group.
271       */
272      public boolean canApply(AnnotationWorkList work) {
273      for (var o : entries)
274         if (o instanceof Serializer.Builder<?> o2 && o2.canApply(work))
275            return true;
276         return false;
277      }
278
279      /**
280       * Clears out any existing serializers in this group.
281       *
282       * @return This object.
283       */
284      public Builder clear() {
285         entries.clear();
286         return this;
287      }
288
289      /**
290       * Makes a copy of this builder.
291       *
292       * @return A new copy of this builder.
293       */
294      public Builder copy() {
295         return new Builder(this);
296      }
297
298      /**
299       * Performs an action on all serializer builders of the specified type in this group.
300       *
301       * @param <T> The serializer builder type.
302       * @param type The serializer builder type.
303       * @param action The action to perform.
304       * @return This object.
305       */
306      public <T extends Serializer.Builder<?>> Builder forEach(Class<T> type, Consumer<T> action) {
307         builders(type).forEach(action);
308         return this;
309      }
310
311      /**
312       * Performs an action on all serializer builders in this group.
313       *
314       * @param action The action to perform.
315       * @return This object.
316       */
317      public Builder forEach(Consumer<Serializer.Builder<?>> action) {
318         builders(Serializer.Builder.class).forEach((Consumer)action);
319         return this;
320      }
321
322      /**
323       * Performs an action on all output stream serializer builders in this group.
324       *
325       * @param action The action to perform.
326       * @return This object.
327       */
328      public Builder forEachOSS(Consumer<OutputStreamSerializer.Builder<?>> action) {
329         return forEach((Class)OutputStreamSerializer.Builder.class, (Consumer)action);
330      }
331
332      /**
333       * Performs an action on all writer serializer builders in this group.
334       *
335       * @param action The action to perform.
336       * @return This object.
337       */
338      public Builder forEachWS(Consumer<WriterSerializer.Builder<?>> action) {
339         return forEach((Class)WriterSerializer.Builder.class, (Consumer)action);
340      }
341
342      /**
343       * Overrides the bean returned by the {@link #build()} method with a pre-built instance.
344       *
345       * @param value The pre-built instance.
346       *    <br>Can be <jk>null</jk> (clears the override; {@link #build()} constructs a new instance).
347       * @return This object.
348       */
349      public Builder impl(Object value) {
350         impl = (SerializerSet) value;
351         return this;
352      }
353
354      /**
355       * Returns direct access to the {@link Serializer} and {@link Serializer.Builder}objects in this builder.
356       *
357       * <p>
358       * Provided to allow for any extraneous modifications to the list not accomplishable via other methods on this builder such
359       * as re-ordering/adding/removing entries.
360       *
361       * <p>
362       * Note that it is up to the user to ensure that the list only contains {@link Serializer} and {@link Serializer.Builder}objects.
363       *
364       * @return The inner list of entries in this builder.
365       */
366      public List<Object> inner() {
367         return entries;
368      }
369
370      /**
371       * Sets the specified serializers for this group.
372       *
373       * <p>
374       * Existing values are overwritten.
375       *
376       * <p>
377       * The {@link Inherit} class can be used to insert existing entries in this group into the position specified.
378       *
379       * <h5 class='section'>Example:</h5>
380       * <p class='bjava'>
381       *    SerializerSet.Builder <jv>builder</jv> = SerializerSet.<jsm>create</jsm>();  <jc>// Create an empty builder.</jc>
382       *
383       *    <jv>builder</jv>.set(FooSerializer.<jk>class</jk>);  <jc>// Now contains:  [FooSerializer]</jc>
384       *
385       *    <jv>builder</jv>.set(BarSerializer.<jk>class</jk>, BazSerializer.<jk>class</jk>);  <jc>// Now contains:  [BarParser,BazSerializer]</jc>
386       *
387       *    <jv>builder</jv>.set(Inherit.<jk>class</jk>, QuxSerializer.<jk>class</jk>);  <jc>// Now contains:  [BarParser,BazSerializer,QuxSerializer]</jc>
388       * </p>
389       *
390       * @param values The serializers to set in this group.
391       * @return This object.
392       * @throws IllegalArgumentException If one or more values do not extend from {@link Serializer} or named <js>"Inherit"</js>.
393       */
394      public Builder set(Class<?>...values) {
395         List<Object> l = list();
396         for (var e : values) {
397            if (e.getSimpleName().equals(CLASS_Inherit)) {
398               l.addAll(entries);
399            } else if (Serializer.class.isAssignableFrom(e)) {
400               l.add(createBuilder(e));
401            } else {
402               throw rex("Invalid type passed to SerializeGroup.Builder.set(): %s", cn(e));
403            }
404         }
405         entries = l;
406         return this;
407      }
408
409      @Override /* Overridden from Object */
410      public String toString() {
411         return entries.stream().map(this::toString).collect(joining(",", "[", "]"));
412      }
413
414
415      private <T extends Serializer.Builder<?>> Stream<T> builders(Class<T> type) {
416         return entries.stream().filter(type::isInstance).map(type::cast);
417      }
418
419      private Object copyBuilder(Object o) {
420         if (o instanceof Serializer.Builder<?> x) {
421            Serializer.Builder<?> x2 = x.copy();
422            if (neq(x.getClass(), x2.getClass()))
423               throw rex("Copy method not implemented on class %s", cn(x));
424            x = x2;
425            if (nn(bcBuilder))
426               x.marshallingContext(bcBuilder);
427            return x;
428         }
429         return o;
430      }
431
432      private Object createBuilder(Object o) {
433         if (o instanceof Class<?> o2) {
434
435            // Check for no-arg constructor.
436            var ci = info(o2).getPublicConstructor(c -> c.getParameterCount() == 0).orElse(null);
437            if (nn(ci))
438               return ci.newInstance();
439
440            // Check for builder create method.
441            Serializer.Builder<?> b = Serializer.createSerializerBuilder((Class<? extends Serializer>)o);
442            if (nn(bcBuilder))
443               b.marshallingContext(bcBuilder);
444            o = b;
445         }
446         return o;
447      }
448
449      private String toString(Object o) {
450         if (o == null)
451            return "null";
452         if (o instanceof Serializer.Builder<?>)
453            return "builder:" + cn(o);
454         return "serializer:" + cn(o);
455      }
456
457      /**
458       * Builds the serializer set.
459       *
460       * @return A new {@link SerializerSet}.
461       */
462      public SerializerSet build() {
463         if (nn(impl))
464            return impl;
465         return new SerializerSet(this);
466      }
467   }
468
469   /**
470    * An identifier that the previous entries in this group should be inherited.
471    * <p>
472    * Used by {@link SerializerSet.Builder#set(Class...)}
473    */
474   public abstract static class Inherit extends Serializer {
475      protected Inherit(Serializer.Builder<?> builder) {
476         super(builder);
477      }
478   }
479
480   /**
481    * An identifier that the previous entries in this group should not be inherited.
482    * <p>
483    * Used by {@link SerializerSet.Builder#add(Class...)}
484    */
485   public abstract static class NoInherit extends Serializer {
486      protected NoInherit(Serializer.Builder<?> builder) {
487         super(builder);
488      }
489   }
490
491   /**
492    * Static creator.
493    *
494    * @return A new builder for this object.
495    */
496   public static Builder create() {
497      return new Builder(BasicBeanStore.INSTANCE);
498   }
499
500   /**
501    * Static creator.
502    *
503    * @param beanStore The bean store to use for creating beans.
504    * @return A new builder for this object.
505    */
506   public static Builder create(BeanStore beanStore) {
507      return new Builder(beanStore);
508   }
509
510   // Maps Accept headers to matching serializers.
511   private final ConcurrentHashMap<String,SerializerMatch> cache = new ConcurrentHashMap<>();
512
513   private final MediaRange[] mediaRanges;
514   private final List<MediaRange> mediaRangesList;
515   private final Serializer[] mediaTypeRangeSerializers;
516
517   private final MediaType[] mediaTypes;
518   private final List<MediaType> mediaTypesList;
519   final Serializer[] entries;
520   private final List<Serializer> entriesList;
521
522   /**
523    * Constructor.
524    *
525    * @param builder The builder for this bean.
526    *    <br>Cannot be <jk>null</jk>.
527    */
528   protected SerializerSet(Builder builder) {
529
530      this.entries = builder.entries.stream().map(this::build).toArray(Serializer[]::new);
531      this.entriesList = u(l(entries));
532
533      List<MediaRange> lmtr = list();
534      Set<MediaType> lmt = set();
535      List<Serializer> l = list();
536      for (var e : entries) {
537         e.getMediaTypeRanges().forEachRange(x -> {
538            lmtr.add(x);
539            l.add(e);
540         });
541         e.forEachAcceptMediaType(lmt::add);
542      }
543
544      this.mediaRanges = lmtr.toArray(new MediaRange[lmtr.size()]);
545      this.mediaRangesList = u(l(mediaRanges));
546      this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
547      this.mediaTypesList = u(l(mediaTypes));
548      this.mediaTypeRangeSerializers = l.toArray(new Serializer[l.size()]);
549   }
550
551   /**
552    * Creates a copy of this serializer group.
553    *
554    * @return A new copy of this serializer group.
555    */
556   public Builder copy() {
557      return new Builder(this);
558   }
559
560   /**
561    * Same as {@link #getSerializerMatch(MediaType)} but returns just the matched serializer.
562    *
563    * @param mediaType The HTTP media type.
564    *    <br>Can be <jk>null</jk> (returns {@link Optional#empty()}).
565    * @return The serializer that matched the accept header, or {@link Optional#empty()} if no match was made.
566    */
567   public Optional<Serializer> getSerializer(MediaType mediaType) {
568      return getSerializerMatch(mediaType).map(SerializerMatch::getSerializer);
569   }
570
571   /**
572    * Same as {@link #getSerializerMatch(String)} but returns just the matched serializer.
573    *
574    * @param acceptHeader The HTTP <l>Accept</l> header string.
575    *    <br>Can be <jk>null</jk> (returns {@link Optional#empty()}).
576    * @return The serializer that matched the accept header, or {@link Optional#empty()} if no match was made.
577    */
578   public Optional<Serializer> getSerializer(String acceptHeader) {
579      return getSerializerMatch(acceptHeader).map(SerializerMatch::getSerializer);
580   }
581
582   /**
583    * Same as {@link #getSerializerMatch(String)} but matches using a {@link MediaType} instance.
584    *
585    * @param mediaType The HTTP media type.  Can be <jk>null</jk> (returns {@link Optional#empty()}).
586    * @return The serializer and media type that matched the media type, or {@link Optional#empty()} if no match was made.
587    */
588   public Optional<SerializerMatch> getSerializerMatch(MediaType mediaType) {
589      return mediaType == null ? oe() : getSerializerMatch(mediaType.toString());
590   }
591
592   /**
593    * Searches the group for a serializer that can handle the specified <c>Accept</c> value.
594    *
595    * <p>
596    * The <c>accept</c> value complies with the syntax described in RFC2616, Section 14.1, as described below:
597    * <p class='bcode'>
598    *    Accept         = "Accept" ":"
599    *                      #( media-range [ accept-params ] )
600    *
601    *    media-range    = ( "*\/*"
602    *                      | ( type "/" "*" )
603    *                      | ( type "/" subtype )
604    *                      ) *( ";" parameter )
605    *    accept-params  = ";" "q" "=" qvalue *( accept-extension )
606    *    accept-extension = ";" token [ "=" ( token | quoted-string ) ]
607    * </p>
608    *
609    * <p>
610    * The returned object includes both the serializer and media type that matched.
611    *
612    * @param acceptHeader The HTTP <l>Accept</l> header string.
613    *    <br>Can be <jk>null</jk> (returns {@link Optional#empty()}).
614    * @return The serializer and media type that matched the accept header, or {@link Optional#empty()} if no match was made.
615    */
616   public Optional<SerializerMatch> getSerializerMatch(String acceptHeader) {
617      if (acceptHeader == null)
618         return oe();
619      SerializerMatch sm = cache.get(acceptHeader);
620      if (nn(sm))
621         return o(sm);
622
623      var a = MediaRanges.of(acceptHeader);
624      int match = a.match(mediaRangesList);
625      if (match >= 0) {
626         sm = new SerializerMatch(mediaRanges[match], mediaTypeRangeSerializers[match]);
627         cache.putIfAbsent(acceptHeader, sm);
628      }
629
630      return o(cache.get(acceptHeader));
631   }
632
633   /**
634    * Returns a copy of the serializers in this group.
635    *
636    * @return An unmodifiable list of serializers in this group.
637    */
638   public List<Serializer> getSerializers() { return entriesList; }
639
640   /**
641    * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link OutputStreamSerializer}.
642    *
643    * @param mediaType The HTTP media type.
644    *    <br>Can be <jk>null</jk> (returns {@link Optional#empty()}).
645    * @return The serializer that matched the accept header, or {@link Optional#empty()} if no match was made.
646    */
647   public Optional<OutputStreamSerializer> getStreamSerializer(MediaType mediaType) {
648      return getSerializer(mediaType).map(OutputStreamSerializer.class::cast);
649   }
650
651   /**
652    * Same as {@link #getSerializer(String)}, but casts it to an {@link OutputStreamSerializer}.
653    *
654    * @param acceptHeader The HTTP <l>Accept</l> header string.
655    *    <br>Can be <jk>null</jk> (returns {@link Optional#empty()}).
656    * @return The serializer that matched the accept header, or {@link Optional#empty()} if no match was made.
657    */
658   public Optional<OutputStreamSerializer> getStreamSerializer(String acceptHeader) {
659      return getSerializer(acceptHeader).map(OutputStreamSerializer.class::cast);
660   }
661
662   /**
663    * Returns the media types that all serializers in this group can handle.
664    *
665    * <p>
666    * Entries are ordered in the same order as the serializers in the group.
667    *
668    * @return An unmodifiable list of media types.
669    */
670   public List<MediaType> getSupportedMediaTypes() { return mediaTypesList; }
671
672   /**
673    * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link WriterSerializer}.
674    *
675    * @param mediaType The HTTP media type.
676    *    <br>Can be <jk>null</jk> (returns {@link Optional#empty()}).
677    * @return The serializer that matched the accept header, or {@link Optional#empty()} if no match was made.
678    */
679   public Optional<WriterSerializer> getWriterSerializer(MediaType mediaType) {
680      return getSerializer(mediaType).map(WriterSerializer.class::cast);
681   }
682
683   /**
684    * Same as {@link #getSerializer(String)}, but casts it to a {@link WriterSerializer}.
685    *
686    * @param acceptHeader The HTTP <l>Accept</l> header string.
687    *    <br>Can be <jk>null</jk> (returns {@link Optional#empty()}).
688    * @return The serializer that matched the accept header, or {@link Optional#empty()} if no match was made.
689    */
690   public Optional<WriterSerializer> getWriterSerializer(String acceptHeader) {
691      return getSerializer(acceptHeader).map(WriterSerializer.class::cast);
692   }
693
694   /**
695    * Returns <jk>true</jk> if this group contains no serializers.
696    *
697    * @return <jk>true</jk> if this group contains no serializers.
698    */
699   public boolean isEmpty() { return entries.length == 0; }
700
701   private Serializer build(Object o) {
702      if (o instanceof Serializer o2)
703         return o2;
704      return ((Serializer.Builder<?>)o).build();
705   }
706}