001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.serializer; 014 015import static org.apache.juneau.internal.CollectionUtils.*; 016 017import java.util.*; 018import java.util.concurrent.*; 019 020import org.apache.juneau.*; 021import org.apache.juneau.http.*; 022 023/** 024 * Represents a group of {@link Serializer Serializers} that can be looked up by media type. 025 * 026 * <h5 class='topic'>Description</h5> 027 * 028 * Provides the following features: 029 * <ul class='spaced-list'> 030 * <li> 031 * Finds serializers based on HTTP <code>Accept</code> header values. 032 * <li> 033 * Sets common properties on all serializers in a single method call. 034 * <li> 035 * Clones existing groups and all serializers within the group in a single method call. 036 * </ul> 037 * 038 * <h5 class='topic'>Match ordering</h5> 039 * 040 * Serializers are matched against <code>Accept</code> strings in the order they exist in this group. 041 * 042 * <p> 043 * Adding new entries will cause the entries to be prepended to the group. 044 * This allows for previous serializers to be overridden through subsequent calls. 045 * 046 * <p> 047 * For example, calling <code>g.append(S1.<jk>class</jk>,S2.<jk>class</jk>).append(S3.<jk>class</jk>,S4.<jk>class</jk>)</code> 048 * will result in the order <code>S3, S4, S1, S2</code>. 049 * 050 * <h5 class='section'>Example:</h5> 051 * <p class='bcode w800'> 052 * <jc>// Construct a new serializer group</jc> 053 * SerializerGroup g = SerializerGroup.<jsm>create</jsm>(); 054 * .append(JsonSerializer.<jk>class</jk>, XmlSerializer.<jk>class</jk>); <jc>// Add some serializers to it</jc> 055 * .ws().pojoSwaps(CalendarSwap.ISO8601DT.<jk>class</jk>) <jc>// Change settings for all serializers in the group.</jc> 056 * .build(); 057 * 058 * <jc>// Find the appropriate serializer by Accept type</jc> 059 * WriterSerializer s = g.getWriterSerializer(<js>"text/foo, text/json;q=0.8, text/*;q:0.6, *\/*;q=0.0"</js>); 060 * 061 * <jc>// Serialize a bean to JSON text </jc> 062 * AddressBook addressBook = <jk>new</jk> AddressBook(); <jc>// Bean to serialize.</jc> 063 * String json = s.serialize(addressBook); 064 * </p> 065 */ 066public final class SerializerGroup extends BeanTraverseContext { 067 068 /** 069 * An unmodifiable empty serializer group. 070 */ 071 public static final SerializerGroup EMPTY = create().build(); 072 073 // Maps Accept headers to matching serializers. 074 private final ConcurrentHashMap<String,SerializerMatch> cache = new ConcurrentHashMap<>(); 075 076 private final MediaTypeRange[] mediaTypeRanges; 077 private final Serializer[] mediaTypeRangeSerializers; 078 079 private final List<MediaType> mediaTypesList; 080 private final List<Serializer> serializers; 081 082 /** 083 * Instantiates a new clean-slate {@link SerializerGroupBuilder} object. 084 * 085 * <p> 086 * This is equivalent to simply calling <code><jk>new</jk> SerializerGroupBuilder()</code>. 087 * 088 * @return A new {@link SerializerGroupBuilder} object. 089 */ 090 public static SerializerGroupBuilder create() { 091 return new SerializerGroupBuilder(); 092 } 093 094 /** 095 * Returns a builder that's a copy of the settings on this serializer group. 096 * 097 * @return A new {@link SerializerGroupBuilder} initialized to this group. 098 */ 099 @Override /* Context */ 100 public SerializerGroupBuilder builder() { 101 return new SerializerGroupBuilder(this); 102 } 103 104 /** 105 * Constructor. 106 * 107 * @param ps 108 * The modifiable properties that were used to initialize the serializers. 109 * A snapshot of these will be made so that we can clone and modify this group. 110 * @param serializers 111 * The serializers defined in this group. 112 * The order is important because they will be tried in reverse order (e.g.newer first) in which they will be tried 113 * to match against media types. 114 */ 115 public SerializerGroup(PropertyStore ps, Serializer[] serializers) { 116 super(ps); 117 this.serializers = immutableList(serializers); 118 119 List<MediaTypeRange> lmtr = new ArrayList<>(); 120 LinkedHashSet<MediaType> lmt = new LinkedHashSet<>(); 121 List<Serializer> l = new ArrayList<>(); 122 for (Serializer s : serializers) { 123 for (MediaTypeRange m: s.getMediaTypeRanges()) { 124 lmtr.add(m); 125 l.add(s); 126 } 127 for (MediaType mt : s.getAcceptMediaTypes()) 128 lmt.add(mt); 129 } 130 131 this.mediaTypeRanges = lmtr.toArray(new MediaTypeRange[lmt.size()]); 132 this.mediaTypesList = unmodifiableList(new ArrayList<>(lmt)); 133 this.mediaTypeRangeSerializers = l.toArray(new Serializer[l.size()]); 134 } 135 136 /** 137 * Searches the group for a serializer that can handle the specified <code>Accept</code> value. 138 * 139 * <p> 140 * The <code>accept</code> value complies with the syntax described in RFC2616, Section 14.1, as described below: 141 * <p class='bcode w800'> 142 * Accept = "Accept" ":" 143 * #( media-range [ accept-params ] ) 144 * 145 * media-range = ( "*\/*" 146 * | ( type "/" "*" ) 147 * | ( type "/" subtype ) 148 * ) *( ";" parameter ) 149 * accept-params = ";" "q" "=" qvalue *( accept-extension ) 150 * accept-extension = ";" token [ "=" ( token | quoted-string ) ] 151 * </p> 152 * 153 * <p> 154 * The returned object includes both the serializer and media type that matched. 155 * 156 * @param acceptHeader The HTTP <l>Accept</l> header string. 157 * @return The serializer and media type that matched the accept header, or <jk>null</jk> if no match was made. 158 */ 159 public SerializerMatch getSerializerMatch(String acceptHeader) { 160 SerializerMatch sm = cache.get(acceptHeader); 161 if (sm != null) 162 return sm; 163 164 Accept a = Accept.forString(acceptHeader); 165 int match = a.findMatch(mediaTypeRanges); 166 if (match >= 0) { 167 sm = new SerializerMatch(mediaTypeRanges[match].getMediaType(), mediaTypeRangeSerializers[match]); 168 cache.putIfAbsent(acceptHeader, sm); 169 } 170 171 return cache.get(acceptHeader); 172 } 173 174 /** 175 * Same as {@link #getSerializerMatch(String)} but matches using a {@link MediaType} instance. 176 * 177 * @param mediaType The HTTP media type. 178 * @return The serializer and media type that matched the media type, or <jk>null</jk> if no match was made. 179 */ 180 public SerializerMatch getSerializerMatch(MediaType mediaType) { 181 return getSerializerMatch(mediaType.toString()); 182 } 183 184 /** 185 * Same as {@link #getSerializerMatch(String)} but returns just the matched serializer. 186 * 187 * @param acceptHeader The HTTP <l>Accept</l> header string. 188 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 189 */ 190 public Serializer getSerializer(String acceptHeader) { 191 SerializerMatch sm = getSerializerMatch(acceptHeader); 192 return sm == null ? null : sm.getSerializer(); 193 } 194 195 /** 196 * Same as {@link #getSerializerMatch(MediaType)} but returns just the matched serializer. 197 * 198 * @param mediaType The HTTP media type. 199 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 200 */ 201 public Serializer getSerializer(MediaType mediaType) { 202 if (mediaType == null) 203 return null; 204 return getSerializer(mediaType.toString()); 205 } 206 207 /** 208 * Same as {@link #getSerializer(String)}, but casts it to a {@link WriterSerializer}. 209 * 210 * @param acceptHeader The HTTP <l>Accept</l> header string. 211 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 212 */ 213 public WriterSerializer getWriterSerializer(String acceptHeader) { 214 return (WriterSerializer)getSerializer(acceptHeader); 215 } 216 217 /** 218 * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link WriterSerializer}. 219 * 220 * @param mediaType The HTTP media type. 221 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 222 */ 223 public WriterSerializer getWriterSerializer(MediaType mediaType) { 224 return (WriterSerializer)getSerializer(mediaType); 225 } 226 227 /** 228 * Same as {@link #getSerializer(String)}, but casts it to an {@link OutputStreamSerializer}. 229 * 230 * @param acceptHeader The HTTP <l>Accept</l> header string. 231 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 232 */ 233 public OutputStreamSerializer getStreamSerializer(String acceptHeader) { 234 return (OutputStreamSerializer)getSerializer(acceptHeader); 235 } 236 237 /** 238 * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link OutputStreamSerializer}. 239 * 240 * @param mediaType The HTTP media type. 241 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 242 */ 243 public OutputStreamSerializer getStreamSerializer(MediaType mediaType) { 244 return (OutputStreamSerializer)getSerializer(mediaType); 245 } 246 247 /** 248 * Returns the media types that all serializers in this group can handle. 249 * 250 * <p> 251 * Entries are ordered in the same order as the serializers in the group. 252 * 253 * @return An unmodifiable list of media types. 254 */ 255 public List<MediaType> getSupportedMediaTypes() { 256 return mediaTypesList; 257 } 258 259 /** 260 * Returns a copy of the serializers in this group. 261 * 262 * @return An unmodifiable list of serializers in this group. 263 */ 264 public List<Serializer> getSerializers() { 265 return serializers; 266 } 267}