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'> 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 BeanContext { 067 068 // Maps Accept headers to matching serializers. 069 private final ConcurrentHashMap<String,SerializerMatch> cache = new ConcurrentHashMap<>(); 070 071 private final MediaType[] mediaTypes; 072 private final List<MediaType> mediaTypesList; 073 private final Serializer[] mediaTypeSerializers; 074 private final List<Serializer> serializers; 075 076 /** 077 * Instantiates a new clean-slate {@link SerializerGroupBuilder} object. 078 * 079 * <p> 080 * This is equivalent to simply calling <code><jk>new</jk> SerializerGroupBuilder()</code>. 081 * 082 * @return A new {@link SerializerGroupBuilder} object. 083 */ 084 public static SerializerGroupBuilder create() { 085 return new SerializerGroupBuilder(); 086 } 087 088 /** 089 * Returns a builder that's a copy of the settings on this serializer group. 090 * 091 * @return A new {@link SerializerGroupBuilder} initialized to this group. 092 */ 093 @Override /* Context */ 094 public SerializerGroupBuilder builder() { 095 return new SerializerGroupBuilder(this); 096 } 097 098 /** 099 * Constructor. 100 * 101 * @param ps 102 * The modifiable properties that were used to initialize the serializers. 103 * A snapshot of these will be made so that we can clone and modify this group. 104 * @param serializers 105 * The serializers defined in this group. 106 * The order is important because they will be tried in reverse order (e.g.newer first) in which they will be tried 107 * to match against media types. 108 */ 109 public SerializerGroup(PropertyStore ps, Serializer[] serializers) { 110 super(ps); 111 this.serializers = immutableList(serializers); 112 113 List<MediaType> lmt = new ArrayList<>(); 114 List<Serializer> l = new ArrayList<>(); 115 for (Serializer s : serializers) { 116 for (MediaType m: s.getMediaTypes()) { 117 lmt.add(m); 118 l.add(s); 119 } 120 } 121 122 this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]); 123 this.mediaTypesList = unmodifiableList(lmt); 124 this.mediaTypeSerializers = l.toArray(new Serializer[l.size()]); 125 } 126 127 /** 128 * Searches the group for a serializer that can handle the specified <code>Accept</code> value. 129 * 130 * <p> 131 * The <code>accept</code> value complies with the syntax described in RFC2616, Section 14.1, as described below: 132 * <p class='bcode'> 133 * Accept = "Accept" ":" 134 * #( media-range [ accept-params ] ) 135 * 136 * media-range = ( "*\/*" 137 * | ( type "/" "*" ) 138 * | ( type "/" subtype ) 139 * ) *( ";" parameter ) 140 * accept-params = ";" "q" "=" qvalue *( accept-extension ) 141 * accept-extension = ";" token [ "=" ( token | quoted-string ) ] 142 * </p> 143 * 144 * <p> 145 * The returned object includes both the serializer and media type that matched. 146 * 147 * @param acceptHeader The HTTP <l>Accept</l> header string. 148 * @return The serializer and media type that matched the accept header, or <jk>null</jk> if no match was made. 149 */ 150 public SerializerMatch getSerializerMatch(String acceptHeader) { 151 SerializerMatch sm = cache.get(acceptHeader); 152 if (sm != null) 153 return sm; 154 155 Accept a = Accept.forString(acceptHeader); 156 int match = a.findMatch(mediaTypes); 157 if (match >= 0) { 158 sm = new SerializerMatch(mediaTypes[match], mediaTypeSerializers[match]); 159 cache.putIfAbsent(acceptHeader, sm); 160 } 161 162 return cache.get(acceptHeader); 163 } 164 165 /** 166 * Same as {@link #getSerializerMatch(String)} but matches using a {@link MediaType} instance. 167 * 168 * @param mediaType The HTTP media type. 169 * @return The serializer and media type that matched the media type, or <jk>null</jk> if no match was made. 170 */ 171 public SerializerMatch getSerializerMatch(MediaType mediaType) { 172 return getSerializerMatch(mediaType.toString()); 173 } 174 175 /** 176 * Same as {@link #getSerializerMatch(String)} but returns just the matched serializer. 177 * 178 * @param acceptHeader The HTTP <l>Accept</l> header string. 179 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 180 */ 181 public Serializer getSerializer(String acceptHeader) { 182 SerializerMatch sm = getSerializerMatch(acceptHeader); 183 return sm == null ? null : sm.getSerializer(); 184 } 185 186 /** 187 * Same as {@link #getSerializerMatch(MediaType)} but returns just the matched serializer. 188 * 189 * @param mediaType The HTTP media type. 190 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 191 */ 192 public Serializer getSerializer(MediaType mediaType) { 193 if (mediaType == null) 194 return null; 195 return getSerializer(mediaType.toString()); 196 } 197 198 /** 199 * Same as {@link #getSerializer(String)}, but casts it to a {@link WriterSerializer}. 200 * 201 * @param acceptHeader The HTTP <l>Accept</l> header string. 202 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 203 */ 204 public WriterSerializer getWriterSerializer(String acceptHeader) { 205 return (WriterSerializer)getSerializer(acceptHeader); 206 } 207 208 /** 209 * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link WriterSerializer}. 210 * 211 * @param mediaType The HTTP media type. 212 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 213 */ 214 public WriterSerializer getWriterSerializer(MediaType mediaType) { 215 return (WriterSerializer)getSerializer(mediaType); 216 } 217 218 /** 219 * Same as {@link #getSerializer(String)}, but casts it to an {@link OutputStreamSerializer}. 220 * 221 * @param acceptHeader The HTTP <l>Accept</l> header string. 222 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 223 */ 224 public OutputStreamSerializer getStreamSerializer(String acceptHeader) { 225 return (OutputStreamSerializer)getSerializer(acceptHeader); 226 } 227 228 /** 229 * Same as {@link #getSerializer(MediaType)}, but casts it to a {@link OutputStreamSerializer}. 230 * 231 * @param mediaType The HTTP media type. 232 * @return The serializer that matched the accept header, or <jk>null</jk> if no match was made. 233 */ 234 public OutputStreamSerializer getStreamSerializer(MediaType mediaType) { 235 return (OutputStreamSerializer)getSerializer(mediaType); 236 } 237 238 /** 239 * Returns the media types that all serializers in this group can handle. 240 * 241 * <p> 242 * Entries are ordered in the same order as the serializers in the group. 243 * 244 * @return An unmodifiable list of media types. 245 */ 246 public List<MediaType> getSupportedMediaTypes() { 247 return mediaTypesList; 248 } 249 250 /** 251 * Returns a copy of the serializers in this group. 252 * 253 * @return An unmodifiable list of serializers in this group. 254 */ 255 public List<Serializer> getSerializers() { 256 return serializers; 257 } 258}