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.parser; 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.annotation.*; 022import org.apache.juneau.http.*; 023 024/** 025 * Represents a group of {@link Parser Parsers} 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 parsers based on HTTP <c>Content-Type</c> header values. 033 * <li> 034 * Sets common properties on all parsers in a single method call. 035 * <li> 036 * Locks all parsers in a single method call. 037 * <li> 038 * Clones existing groups and all parsers within the group in a single method call. 039 * </ul> 040 * 041 * <h5 class='topic'>Match ordering</h5> 042 * 043 * Parsers are matched against <c>Content-Type</c> strings in the order they exist in this group. 044 * 045 * <p> 046 * Adding new entries will cause the entries to be prepended to the group. 047 * This allows for previous parsers to be overridden through subsequent calls. 048 * 049 * <p> 050 * For example, calling <code>g.append(P1.<jk>class</jk>,P2.<jk>class</jk>).append(P3.<jk>class</jk>,P4.<jk>class</jk>)</code> 051 * will result in the order <c>P3, P4, P1, P2</c>. 052 * 053 * <h5 class='section'>Example:</h5> 054 * <p class='bcode w800'> 055 * <jc>// Construct a new parser group builder</jc> 056 * ParserGroupBuilder b = ParserGroup.<jsm>create</jsm>(); 057 * 058 * <jc>// Add some parsers to it</jc> 059 * b.append(JsonParser.<jk>class</jk>, XmlParser.<jk>class</jk>); 060 * 061 * <jc>// Change settings on parsers simultaneously</jc> 062 * b.set(BeanContext.<jsf>BEAN_beansRequireSerializable</jsf>, <jk>true</jk>) 063 * .pojoSwaps(TemporalCalendarSwap.IsoLocalDateTime.<jk>class</jk>); 064 * 065 * ParserGroup g = b.build(); 066 * 067 * <jc>// Find the appropriate parser by Content-Type</jc> 068 * ReaderParser p = (ReaderParser)g.getParser(<js>"text/json"</js>); 069 * 070 * <jc>// Parse a bean from JSON</jc> 071 * String json = <js>"{...}"</js>; 072 * AddressBook addressBook = p.parse(json, AddressBook.<jk>class</jk>); 073 * </p> 074 */ 075@ConfigurableContext(nocache=true) 076public final class ParserGroup extends BeanContext { 077 078 /** 079 * An unmodifiable empty parser group. 080 */ 081 public static final ParserGroup EMPTY = create().build(); 082 083 // Maps Content-Type headers to matches. 084 private final ConcurrentHashMap<String,ParserMatch> cache = new ConcurrentHashMap<>(); 085 086 private final MediaType[] mediaTypes; // List of media types 087 private final List<MediaType> mediaTypesList; 088 private final Parser[] mediaTypeParsers; 089 private final List<Parser> parsers; 090 091 /** 092 * Instantiates a new clean-slate {@link ParserGroupBuilder} object. 093 * 094 * <p> 095 * This is equivalent to simply calling <code><jk>new</jk> ParserGroupBuilder()</code>. 096 * 097 * @return A new {@link ParserGroupBuilder} object. 098 */ 099 public static ParserGroupBuilder create() { 100 return new ParserGroupBuilder(); 101 } 102 103 /** 104 * Returns a builder that's a copy of the settings on this parser group. 105 * 106 * @return A new {@link ParserGroupBuilder} initialized to this group. 107 */ 108 @Override /* Context */ 109 public ParserGroupBuilder builder() { 110 return new ParserGroupBuilder(this); 111 } 112 113 /** 114 * Constructor. 115 * 116 * @param ps 117 * The modifiable properties that were used to initialize the parsers. 118 * A snapshot of these will be made so that we can clone and modify this group. 119 * @param parsers 120 * The parsers defined in this group. 121 * The order is important because they will be tried in reverse order (e.g. newer first) in which they will be 122 * tried to match against media types. 123 */ 124 public ParserGroup(PropertyStore ps, Parser[] parsers) { 125 super(ps); 126 this.parsers = immutableList(parsers); 127 128 List<MediaType> lmt = new ArrayList<>(); 129 List<Parser> l = new ArrayList<>(); 130 for (Parser p : parsers) { 131 for (MediaType m: p.getMediaTypes()) { 132 lmt.add(m); 133 l.add(p); 134 } 135 } 136 137 this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]); 138 this.mediaTypesList = unmodifiableList(lmt); 139 this.mediaTypeParsers = l.toArray(new Parser[l.size()]); 140 } 141 142 /** 143 * Searches the group for a parser that can handle the specified <l>Content-Type</l> header value. 144 * 145 * <p> 146 * The returned object includes both the parser and media type that matched. 147 * 148 * @param contentTypeHeader The HTTP <l>Content-Type</l> header value. 149 * @return The parser and media type that matched the content type header, or <jk>null</jk> if no match was made. 150 */ 151 public ParserMatch getParserMatch(String contentTypeHeader) { 152 ParserMatch pm = cache.get(contentTypeHeader); 153 if (pm != null) 154 return pm; 155 156 ContentType ct = ContentType.forString(contentTypeHeader); 157 int match = ct.findMatch(mediaTypes); 158 159 if (match >= 0) { 160 pm = new ParserMatch(mediaTypes[match], mediaTypeParsers[match]); 161 cache.putIfAbsent(contentTypeHeader, pm); 162 } 163 164 return cache.get(contentTypeHeader); 165 } 166 167 /** 168 * Same as {@link #getParserMatch(String)} but matches using a {@link MediaType} instance. 169 * 170 * @param mediaType The HTTP <l>Content-Type</l> header value as a media type. 171 * @return The parser and media type that matched the media type, or <jk>null</jk> if no match was made. 172 */ 173 public ParserMatch getParserMatch(MediaType mediaType) { 174 return getParserMatch(mediaType.toString()); 175 } 176 177 /** 178 * Same as {@link #getParserMatch(String)} but returns just the matched parser. 179 * 180 * @param contentTypeHeader The HTTP <l>Content-Type</l> header string. 181 * @return The parser that matched the content type header, or <jk>null</jk> if no match was made. 182 */ 183 public Parser getParser(String contentTypeHeader) { 184 ParserMatch pm = getParserMatch(contentTypeHeader); 185 return pm == null ? null : pm.getParser(); 186 } 187 188 /** 189 * Same as {@link #getParserMatch(MediaType)} but returns just the matched parser. 190 * 191 * @param mediaType The HTTP media type. 192 * @return The parser that matched the media type, or <jk>null</jk> if no match was made. 193 */ 194 public Parser getParser(MediaType mediaType) { 195 ParserMatch pm = getParserMatch(mediaType); 196 return pm == null ? null : pm.getParser(); 197 } 198 199 /** 200 * Returns the media types that all parsers in this group can handle 201 * 202 * <p> 203 * Entries are ordered in the same order as the parsers in the group. 204 * 205 * @return An unmodifiable list of media types. 206 */ 207 public List<MediaType> getSupportedMediaTypes() { 208 return mediaTypesList; 209 } 210 211 /** 212 * Returns the parsers in this group. 213 * 214 * @return An unmodifiable list of parsers in this group. 215 */ 216 public List<Parser> getParsers() { 217 return parsers; 218 } 219}