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