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'> 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 // Maps Content-Type headers to matches. 077 private final ConcurrentHashMap<String,ParserMatch> cache = new ConcurrentHashMap<>(); 078 079 private final MediaType[] mediaTypes; // List of media types 080 private final List<MediaType> mediaTypesList; 081 private final Parser[] mediaTypeParsers; 082 private final List<Parser> parsers; 083 084 /** 085 * Instantiates a new clean-slate {@link ParserGroupBuilder} object. 086 * 087 * <p> 088 * This is equivalent to simply calling <code><jk>new</jk> ParserGroupBuilder()</code>. 089 * 090 * @return A new {@link ParserGroupBuilder} object. 091 */ 092 public static ParserGroupBuilder create() { 093 return new ParserGroupBuilder(); 094 } 095 096 /** 097 * Returns a builder that's a copy of the settings on this parser group. 098 * 099 * @return A new {@link ParserGroupBuilder} initialized to this group. 100 */ 101 @Override /* Context */ 102 public ParserGroupBuilder builder() { 103 return new ParserGroupBuilder(this); 104 } 105 106 /** 107 * Constructor. 108 * 109 * @param ps 110 * The modifiable properties that were used to initialize the parsers. 111 * A snapshot of these will be made so that we can clone and modify this group. 112 * @param parsers 113 * The parsers 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 115 * tried to match against media types. 116 */ 117 public ParserGroup(PropertyStore ps, Parser[] parsers) { 118 super(ps); 119 this.parsers = immutableList(parsers); 120 121 List<MediaType> lmt = new ArrayList<>(); 122 List<Parser> l = new ArrayList<>(); 123 for (Parser p : parsers) { 124 for (MediaType m: p.getMediaTypes()) { 125 lmt.add(m); 126 l.add(p); 127 } 128 } 129 130 this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]); 131 this.mediaTypesList = unmodifiableList(lmt); 132 this.mediaTypeParsers = l.toArray(new Parser[l.size()]); 133 } 134 135 /** 136 * Searches the group for a parser that can handle the specified <l>Content-Type</l> header value. 137 * 138 * <p> 139 * The returned object includes both the parser and media type that matched. 140 * 141 * @param contentTypeHeader The HTTP <l>Content-Type</l> header value. 142 * @return The parser and media type that matched the content type header, or <jk>null</jk> if no match was made. 143 */ 144 public ParserMatch getParserMatch(String contentTypeHeader) { 145 ParserMatch pm = cache.get(contentTypeHeader); 146 if (pm != null) 147 return pm; 148 149 ContentType ct = ContentType.forString(contentTypeHeader); 150 int match = ct.findMatch(mediaTypes); 151 152 if (match >= 0) { 153 pm = new ParserMatch(mediaTypes[match], mediaTypeParsers[match]); 154 cache.putIfAbsent(contentTypeHeader, pm); 155 } 156 157 return cache.get(contentTypeHeader); 158 } 159 160 /** 161 * Same as {@link #getParserMatch(String)} but matches using a {@link MediaType} instance. 162 * 163 * @param mediaType The HTTP <l>Content-Type</l> header value as a media type. 164 * @return The parser and media type that matched the media type, or <jk>null</jk> if no match was made. 165 */ 166 public ParserMatch getParserMatch(MediaType mediaType) { 167 return getParserMatch(mediaType.toString()); 168 } 169 170 /** 171 * Same as {@link #getParserMatch(String)} but returns just the matched parser. 172 * 173 * @param contentTypeHeader The HTTP <l>Content-Type</l> header string. 174 * @return The parser that matched the content type header, or <jk>null</jk> if no match was made. 175 */ 176 public Parser getParser(String contentTypeHeader) { 177 ParserMatch pm = getParserMatch(contentTypeHeader); 178 return pm == null ? null : pm.getParser(); 179 } 180 181 /** 182 * Same as {@link #getParserMatch(MediaType)} but returns just the matched parser. 183 * 184 * @param mediaType The HTTP media type. 185 * @return The parser that matched the media type, or <jk>null</jk> if no match was made. 186 */ 187 public Parser getParser(MediaType mediaType) { 188 ParserMatch pm = getParserMatch(mediaType); 189 return pm == null ? null : pm.getParser(); 190 } 191 192 /** 193 * Returns the media types that all parsers in this group can handle 194 * 195 * <p> 196 * Entries are ordered in the same order as the parsers in the group. 197 * 198 * @return An unmodifiable list of media types. 199 */ 200 public List<MediaType> getSupportedMediaTypes() { 201 return mediaTypesList; 202 } 203 204 /** 205 * Returns the parsers in this group. 206 * 207 * @return An unmodifiable list of parsers in this group. 208 */ 209 public List<Parser> getParsers() { 210 return parsers; 211 } 212}