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