001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.http; 014 015import java.util.Collections; 016import java.util.List; 017import java.util.Map; 018import java.util.Objects; 019import java.util.StringTokenizer; 020import java.util.regex.Matcher; 021import java.util.regex.Pattern; 022 023import org.apache.tapestry5.commons.util.CollectionFactory; 024import org.apache.tapestry5.http.internal.TapestryHttpInternalConstants; 025import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026 027/** 028 * Represents an HTTP content type. Allows to set various elements like the MIME type, the character set, and other 029 * parameters. This is similar to a number of other implementations of the same concept in JAF, etc. We have created 030 * this simple implementation to avoid including the whole libraries. 031 * 032 * As of Tapestry 5.4, this is now an immutable data type. 033 */ 034public final class ContentType 035{ 036 private final String baseType; 037 038 private final String subType; 039 040 private final Map<String, String> parameters; 041 042 private static final Pattern PATTERN = Pattern.compile("^(.+)/([^;]+)(;(.+=[^;]+)){0,3}$"); 043 044 /** 045 * Creates a new content type from the argument. The format of the argument has to be basetype/subtype(;key=value)* 046 * 047 * @param contentType 048 * the content type that needs to be represented 049 */ 050 public ContentType(String contentType) 051 { 052 Matcher matcher = PATTERN.matcher(contentType); 053 054 if (!matcher.matches()) 055 { 056 throw new IllegalArgumentException(String.format("Not a parseable content type '%s'.", contentType)); 057 } 058 059 this.baseType = matcher.group(1); 060 this.subType = matcher.group(2); 061 this.parameters = parseKeyValues(matcher.group(4)); 062 } 063 064 private ContentType(String baseType, String subType, Map<String, String> parameters) 065 { 066 this.baseType = baseType; 067 this.subType = subType; 068 this.parameters = parameters; 069 } 070 071 072 private static Map<String, String> parseKeyValues(String keyValues) 073 { 074 if (keyValues == null) 075 { 076 return Collections.emptyMap(); 077 } 078 079 Map<String, String> parameters = CollectionFactory.newCaseInsensitiveMap(); 080 081 StringTokenizer tk = new StringTokenizer(keyValues, ";"); 082 083 while (tk.hasMoreTokens()) 084 { 085 String token = tk.nextToken(); 086 int sep = token.indexOf('='); 087 088 parameters.put(token.substring(0, sep), token.substring(sep + 1)); 089 } 090 091 return parameters; 092 } 093 094 /** 095 * Returns true only if the other object is another instance of ContentType, and has the same baseType, subType and 096 * set of parameters. 097 */ 098 @Override 099 public boolean equals(Object o) 100 { 101 if (o == null) return false; 102 103 if (o.getClass() != this.getClass()) return false; 104 105 ContentType ct = (ContentType) o; 106 107 return baseType.equals(ct.baseType) && subType.equals(ct.subType) && parameters.equals(ct.parameters); 108 } 109 110 @Override 111 public int hashCode() 112 { 113 return Objects.hash(baseType, subType, parameters); 114 } 115 116 /** 117 * @return the base type of the content type 118 */ 119 public String getBaseType() 120 { 121 return baseType; 122 } 123 124 /** 125 * @return the sub-type of the content type 126 */ 127 public String getSubType() 128 { 129 return subType; 130 } 131 132 /** 133 * @return the MIME type of the content type (the base type and the subtype, seperated with a '/'). 134 */ 135 public String getMimeType() 136 { 137 return baseType + "/" + subType; 138 } 139 140 /** 141 * @return the list of names of parameters in this content type, in alphabetical order. 142 */ 143 public List<String> getParameterNames() 144 { 145 return InternalUtils.sortedKeys(parameters); 146 } 147 148 /** 149 * @return the character set (the "charset" parameter) or null. 150 */ 151 public String getCharset() 152 { 153 return getParameter(TapestryHttpInternalConstants.CHARSET_CONTENT_TYPE_PARAMETER); 154 } 155 156 /** 157 * @param key 158 * the name of the content type parameter 159 * @return the value of the content type parameter 160 */ 161 public String getParameter(String key) 162 { 163 assert key != null; 164 return parameters.get(key); 165 } 166 167 private String unparse() 168 { 169 StringBuilder buffer = new StringBuilder(getMimeType()); 170 171 for (String parameterName : getParameterNames()) 172 { 173 buffer.append(';'); 174 buffer.append(parameterName); 175 buffer.append('='); 176 buffer.append(parameters.get(parameterName)); 177 } 178 179 return buffer.toString(); 180 } 181 182 /** 183 * Returns a new content type with the indicated parameter. 184 * 185 * @since 5.4 186 */ 187 public ContentType withParameter(String key, String value) 188 { 189 assert InternalUtils.isNonBlank(key); 190 assert InternalUtils.isNonBlank(value); 191 192 Map<String, String> newParameters = CollectionFactory.newCaseInsensitiveMap(); 193 194 newParameters.putAll(parameters); 195 newParameters.put(key, value); 196 197 return new ContentType(baseType, subType, newParameters); 198 } 199 200 public ContentType withCharset(String charset) 201 { 202 return withParameter(TapestryHttpInternalConstants.CHARSET_CONTENT_TYPE_PARAMETER, charset); 203 } 204 205 /** 206 * @return the string representation of this content type. 207 */ 208 @Override 209 public String toString() 210 { 211 return unparse(); 212 } 213 214 /** 215 * @return true if the content type includes parameters (such as 'charset'). 216 * @since 5.4 217 */ 218 public boolean hasParameters() 219 { 220 return !parameters.isEmpty(); 221 } 222}