// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.serializer;
import static java.util.stream.Collectors.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static org.apache.juneau.internal.ObjectUtils.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
import org.apache.juneau.*;
import org.apache.juneau.cp.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.reflect.*;
/**
* Represents a group of {@link Serializer Serializers} that can be looked up by media type.
*
* Description
*
* Provides the following features:
*
*
* Finds serializers based on HTTP Accept header values.
*
* Sets common properties on all serializers in a single method call.
*
* Clones existing groups and all serializers within the group in a single method call.
*
*
* Match ordering
*
* Serializers are matched against Accept strings in the order they exist in this group.
*
*
* Adding new entries will cause the entries to be prepended to the group.
* This allows for previous serializers to be overridden through subsequent calls.
*
*
* For example, calling g.append(S1.class ,S2.class ).append(S3.class ,S4.class )
* will result in the order S3, S4, S1, S2 .
*
*
Example:
*
* // Construct a new serializer group
* SerializerSet serializers = SerializerSet.create ();
* .add(JsonSerializer.class , UrlEncodingSerializer.class ) // Add some serializers to it
* .forEach(x -> x .swaps(TemporalCalendarSwap.IsoLocalDateTime.class ))
* .forEachWS(x -> x .ws())
* .build();
*
* // Find the appropriate serializer by Accept type
* WriterSerializer serializer = serializers
* .getWriterSerializer("text/foo, text/json;q=0.8, text/*;q:0.6, *\/*;q=0.0" );
*
* // Serialize a bean to JSON text
* AddressBook addressBook = new AddressBook(); // Bean to serialize.
* String json = serializer .serialize(addressBook );
*
*
* Notes:
* This class is thread safe and reusable.
*
*
* See Also:
*/
public final class SerializerSet {
//-----------------------------------------------------------------------------------------------------------------
// Static
//-----------------------------------------------------------------------------------------------------------------
/**
* An identifier that the previous entries in this group should be inherited.
*
* Used by {@link SerializerSet.Builder#set(Class...)}
*/
@SuppressWarnings("javadoc")
public static abstract class Inherit extends Serializer {
public Inherit(Serializer.Builder builder) {
super(builder);
}
}
/**
* An identifier that the previous entries in this group should not be inherited.
*
* Used by {@link SerializerSet.Builder#add(Class...)}
*/
@SuppressWarnings("javadoc")
public static abstract class NoInherit extends Serializer {
public NoInherit(Serializer.Builder builder) {
super(builder);
}
}
/**
* Static creator.
*
* @param beanStore The bean store to use for creating beans.
* @return A new builder for this object.
*/
public static Builder create(BeanStore beanStore) {
return new Builder(beanStore);
}
/**
* Static creator.
*
* @return A new builder for this object.
*/
public static Builder create() {
return new Builder(BeanStore.INSTANCE);
}
//-----------------------------------------------------------------------------------------------------------------
// Builder
//-----------------------------------------------------------------------------------------------------------------
/**
* Builder class.
*/
@FluentSetters
public static class Builder extends BeanBuilder {
List entries;
private BeanContext.Builder bcBuilder;
/**
* Create an empty serializer group builder.
*
* @param beanStore The bean store to use for creating beans.
*/
protected Builder(BeanStore beanStore) {
super(SerializerSet.class, beanStore);
this.entries = list();
}
/**
* Clone an existing serializer group.
*
* @param copyFrom The serializer group that we're copying settings and serializers from.
*/
protected Builder(SerializerSet copyFrom) {
super(copyFrom.getClass());
this.entries = list((Object[])copyFrom.entries);
}
/**
* Clone an existing serializer group builder.
*
*
* Serializer builders will be cloned during this process.
*
* @param copyFrom The serializer group that we're copying settings and serializers from.
*/
protected Builder(Builder copyFrom) {
super(copyFrom);
bcBuilder = copyFrom.bcBuilder == null ? null : copyFrom.bcBuilder.copy();
entries = list();
copyFrom.entries.stream().map(this::copyBuilder).forEach(x -> entries.add(x));
}
private Object copyBuilder(Object o) {
if (o instanceof Serializer.Builder) {
Serializer.Builder x = (Serializer.Builder)o;
Serializer.Builder x2 = x.copy();
if (ne(x.getClass(), x2.getClass()))
throw new BasicRuntimeException("Copy method not implemented on class {0}", x.getClass().getName());
x = x2;
if (bcBuilder != null)
x.beanContext(bcBuilder);
return x;
}
return o;
}
@Override /* BeanBuilder */
protected SerializerSet buildDefault() {
return new SerializerSet(this);
}
/**
* Makes a copy of this builder.
*
* @return A new copy of this builder.
*/
public Builder copy() {
return new Builder(this);
}
//-------------------------------------------------------------------------------------------------------------
// Properties
//-------------------------------------------------------------------------------------------------------------
/**
* Associates an existing bean context builder with all serializer builders in this group.
*
* @param value The bean contest builder to associate.
* @return This object.
*/
public Builder beanContext(BeanContext.Builder value) {
bcBuilder = value;
forEach(x -> x.beanContext(value));
return this;
}
/**
* Applies an operation to the bean context builder.
*
* @param operation The operation to apply.
* @return This object.
*/
public final Builder beanContext(Consumer operation) {
if (bcBuilder != null)
operation.accept(bcBuilder);
return this;
}
/**
* Adds the specified serializers to this group.
*
*
* Entries are added in-order to the beginning of the list in the group.
*
*
* The {@link NoInherit} class can be used to clear out the existing list of serializers before adding the new entries.
*
*
Example:
*
* SerializerSet.Builder builder = SerializerSet.create (); // Create an empty builder.
*
* builder .add(FooSerializer.class ); // Now contains: [FooSerializer]
*
* builder .add(BarSerializer.class , BazSerializer.class ); // Now contains: [BarParser,BazSerializer,FooSerializer]
*
* builder .add(NoInherit.class , QuxSerializer.class ); // Now contains: [QuxSerializer]
*
*
* @param values The serializers to add to this group.
* @return This object.
* @throws IllegalArgumentException If one or more values do not extend from {@link Serializer}.
*/
public Builder add(Class>...values) {
List l = list();
for (Class> e : values) {
if (Serializer.class.isAssignableFrom(e)) {
l.add(createBuilder(e));
} else {
throw new BasicRuntimeException("Invalid type passed to SerializeGroup.Builder.add(): {0}", e.getName());
}
}
entries.addAll(0, l);
return this;
}
/**
* Sets the specified serializers for this group.
*
*
* Existing values are overwritten.
*
*
* The {@link Inherit} class can be used to insert existing entries in this group into the position specified.
*
*
Example:
*
* SerializerSet.Builder builder = SerializerSet.create (); // Create an empty builder.
*
* builder .set(FooSerializer.class ); // Now contains: [FooSerializer]
*
* builder .set(BarSerializer.class , BazSerializer.class ); // Now contains: [BarParser,BazSerializer]
*
* builder .set(Inherit.class , QuxSerializer.class ); // Now contains: [BarParser,BazSerializer,QuxSerializer]
*
*
* @param values The serializers to set in this group.
* @return This object.
* @throws IllegalArgumentException If one or more values do not extend from {@link Serializer} or named "Inherit" .
*/
public Builder set(Class>...values) {
List l = list();
for (Class> e : values) {
if (e.getSimpleName().equals("Inherit")) {
l.addAll(entries);
} else if (Serializer.class.isAssignableFrom(e)) {
l.add(createBuilder(e));
} else {
throw new BasicRuntimeException("Invalid type passed to SerializeGroup.Builder.set(): {0}", e.getName());
}
}
entries = l;
return this;
}
private Object createBuilder(Object o) {
if (o instanceof Class) {
// Check for no-arg constructor.
ConstructorInfo ci = ClassInfo.of((Class>)o).getPublicConstructor(ConstructorInfo::hasNoParams);
if (ci != null)
return ci.invoke();
// Check for builder create method.
@SuppressWarnings("unchecked")
Serializer.Builder b = Serializer.createSerializerBuilder((Class extends Serializer>)o);
if (bcBuilder != null)
b.beanContext(bcBuilder);
o = b;
}
return o;
}
/**
* Registers the specified serializers with this group.
*
*
* When passing in pre-instantiated serializers to this group, applying properties and transforms to the group
* do not affect them.
*
* @param s The serializers to append to this group.
* @return This object.
*/
public Builder add(Serializer...s) {
prependAll(entries, (Object[])s);
return this;
}
/**
* Clears out any existing serializers in this group.
*
* @return This object.
*/
public Builder clear() {
entries.clear();
return this;
}
/**
* Returns true if at least one of the specified annotations can be applied to at least one serializer builder in this group.
*
* @param work The work to check.
* @return true if at least one of the specified annotations can be applied to at least one serializer builder in this group.
*/
public boolean canApply(AnnotationWorkList work) {
for (Object o : entries)
if (o instanceof Serializer.Builder)
if (((Serializer.Builder)o).canApply(work))
return true;
return false;
}
/**
* Applies the specified annotations to all applicable serializer builders in this group.
*
* @param work The annotations to apply.
* @return This object.
*/
public Builder apply(AnnotationWorkList work) {
return forEach(x -> x.apply(work));
}
/**
* Performs an action on all serializer builders in this group.
*
* @param action The action to perform.
* @return This object.
*/
public Builder forEach(Consumer action) {
builders(Serializer.Builder.class).forEach(action);
return this;
}
/**
* Performs an action on all writer serializer builders in this group.
*
* @param action The action to perform.
* @return This object.
*/
public Builder forEachWS(Consumer action) {
return forEach(WriterSerializer.Builder.class, action);
}
/**
* Performs an action on all output stream serializer builders in this group.
*
* @param action The action to perform.
* @return This object.
*/
public Builder forEachOSS(Consumer action) {
return forEach(OutputStreamSerializer.Builder.class, action);
}
/**
* Performs an action on all serializer builders of the specified type in this group.
*
* @param The serializer builder type.
* @param type The serializer builder type.
* @param action The action to perform.
* @return This object.
*/
public Builder forEach(Class type, Consumer action) {
builders(type).forEach(action);
return this;
}
/**
* Returns direct access to the {@link Serializer} and {@link Serializer.Builder} objects in this builder.
*
*
* Provided to allow for any extraneous modifications to the list not accomplishable via other methods on this builder such
* as re-ordering/adding/removing entries.
*
*
* Note that it is up to the user to ensure that the list only contains {@link Serializer} and {@link Serializer.Builder} objects.
*
* @return The inner list of entries in this builder.
*/
public List inner() {
return entries;
}
private Stream builders(Class type) {
return entries.stream().filter(x -> type.isInstance(x)).map(x -> type.cast(x));
}
//
@Override /* GENERATED - org.apache.juneau.BeanBuilder */
public Builder impl(Object value) {
super.impl(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanBuilder */
public Builder type(Class> value) {
super.type(value);
return this;
}
//
//-------------------------------------------------------------------------------------------------------------
// Other methods
//-------------------------------------------------------------------------------------------------------------
@Override /* Object */
public String toString() {
return entries.stream().map(this::toString).collect(joining(",","[","]"));
}
private String toString(Object o) {
if (o == null)
return "null";
if (o instanceof Serializer.Builder)
return "builder:" + o.getClass().getName();
return "serializer:" + o.getClass().getName();
}
}
//-----------------------------------------------------------------------------------------------------------------
// Instance
//-----------------------------------------------------------------------------------------------------------------
// Maps Accept headers to matching serializers.
private final ConcurrentHashMap cache = new ConcurrentHashMap<>();
private final MediaRange[] mediaRanges;
private final List mediaRangesList;
private final Serializer[] mediaTypeRangeSerializers;
private final MediaType[] mediaTypes;
private final List mediaTypesList;
final Serializer[] entries;
private final List entriesList;
/**
* Constructor.
*
* @param builder The builder for this bean.
*/
protected SerializerSet(Builder builder) {
this.entries = builder.entries.stream().map(this::build).toArray(Serializer[]::new);
this.entriesList = ulist(entries);
List lmtr = list();
Set lmt = set();
List l = list();
for (Serializer e : entries) {
e.getMediaTypeRanges().forEachRange(x -> {
lmtr.add(x);
l.add(e);
});
e.forEachAcceptMediaType(x -> lmt.add(x));
}
this.mediaRanges = lmtr.toArray(new MediaRange[lmtr.size()]);
this.mediaRangesList = ulist(mediaRanges);
this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
this.mediaTypesList = ulist(mediaTypes);
this.mediaTypeRangeSerializers = l.toArray(new Serializer[l.size()]);
}
private Serializer build(Object o) {
if (o instanceof Serializer)
return (Serializer)o;
return ((Serializer.Builder)o).build();
}
/**
* Creates a copy of this serializer group.
*
* @return A new copy of this serializer group.
*/
public Builder copy() {
return new Builder(this);
}
/**
* Searches the group for a serializer that can handle the specified Accept value.
*
*
* The accept value complies with the syntax described in RFC2616, Section 14.1, as described below:
*
* Accept = "Accept" ":"
* #( media-range [ accept-params ] )
*
* media-range = ( "*\/*"
* | ( type "/" "*" )
* | ( type "/" subtype )
* ) *( ";" parameter )
* accept-params = ";" "q" "=" qvalue *( accept-extension )
* accept-extension = ";" token [ "=" ( token | quoted-string ) ]
*
*
*
* The returned object includes both the serializer and media type that matched.
*
* @param acceptHeader The HTTP Accept header string.
* @return The serializer and media type that matched the accept header, or null if no match was made.
*/
public SerializerMatch getSerializerMatch(String acceptHeader) {
if (acceptHeader == null)
return null;
SerializerMatch sm = cache.get(acceptHeader);
if (sm != null)
return sm;
MediaRanges a = MediaRanges.of(acceptHeader);
int match = a.match(mediaRangesList);
if (match >= 0) {
sm = new SerializerMatch(mediaRanges[match], mediaTypeRangeSerializers[match]);
cache.putIfAbsent(acceptHeader, sm);
}
return cache.get(acceptHeader);
}
/**
* Same as {@link #getSerializerMatch(String)} but matches using a {@link MediaType} instance.
*
* @param mediaType The HTTP media type.
* @return The serializer and media type that matched the media type, or null if no match was made.
*/
public SerializerMatch getSerializerMatch(MediaType mediaType) {
return getSerializerMatch(mediaType.toString());
}
/**
* Same as {@link #getSerializerMatch(String)} but returns just the matched serializer.
*
* @param acceptHeader The HTTP Accept header string.
* @return The serializer that matched the accept header, or null if no match was made.
*/
public Serializer getSerializer(String acceptHeader) {
SerializerMatch sm = getSerializerMatch(acceptHeader);
return sm == null ? null : sm.getSerializer();
}
/**
* Same as {@link #getSerializerMatch(MediaType)} but returns just the matched serializer.
*
* @param mediaType The HTTP media type.
* @return The serializer that matched the accept header, or null if no match was made.
*/
public Serializer getSerializer(MediaType mediaType) {
if (mediaType == null)
return null;
return getSerializer(mediaType.toString());
}
/**
* Same as {@link #getSerializer(String)}, but casts it to a {@link WriterSerializer}.
*
* @param acceptHeader The HTTP Accept header string.
* @return The serializer that matched the accept header, or null if no match was made.
*/
public WriterSerializer getWriterSerializer(String acceptHeader) {
return (WriterSerializer)getSerializer(acceptHeader);
}
/**
* Same as {@link #getSerializer(MediaType)}, but casts it to a {@link WriterSerializer}.
*
* @param mediaType The HTTP media type.
* @return The serializer that matched the accept header, or null if no match was made.
*/
public WriterSerializer getWriterSerializer(MediaType mediaType) {
return (WriterSerializer)getSerializer(mediaType);
}
/**
* Same as {@link #getSerializer(String)}, but casts it to an {@link OutputStreamSerializer}.
*
* @param acceptHeader The HTTP Accept header string.
* @return The serializer that matched the accept header, or null if no match was made.
*/
public OutputStreamSerializer getStreamSerializer(String acceptHeader) {
return (OutputStreamSerializer)getSerializer(acceptHeader);
}
/**
* Same as {@link #getSerializer(MediaType)}, but casts it to a {@link OutputStreamSerializer}.
*
* @param mediaType The HTTP media type.
* @return The serializer that matched the accept header, or null if no match was made.
*/
public OutputStreamSerializer getStreamSerializer(MediaType mediaType) {
return (OutputStreamSerializer)getSerializer(mediaType);
}
/**
* Returns the media types that all serializers in this group can handle.
*
*
* Entries are ordered in the same order as the serializers in the group.
*
* @return An unmodifiable list of media types.
*/
public List getSupportedMediaTypes() {
return mediaTypesList;
}
/**
* Returns a copy of the serializers in this group.
*
* @return An unmodifiable list of serializers in this group.
*/
public List getSerializers() {
return entriesList;
}
/**
* Returns true if this group contains no serializers.
*
* @return true if this group contains no serializers.
*/
public boolean isEmpty() {
return entries.length == 0;
}
}