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