001/**
002 * Java Web Archive Toolkit - Software to read and validate ARC, WARC
003 * and GZip files. (http://jwat.org/)
004 * Copyright 2011-2012 Netarkivet.dk (http://netarkivet.dk/)
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.jwat.common;
019
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.Map;
023import java.util.Map.Entry;
024
025/**
026 * This class represents a content-type string parsed, validated and decomposed
027 * into its separate components. It's based on the rfc2616 text and accordingly
028 * fairly strict concerning whitespaces. Whitespace is only permissible after
029 * a ';' and before and after the whole content-type string.
030 * ContentType, MediaType and parameter names are all converted to lower case.
031 *
032 * @author nicl
033 */
034public class ContentType {
035
036    /** Beginning state. */
037    protected static final int S_START = 0;
038    /** Parsing content-type state. */
039    protected static final int S_CONTENTTYPE = 1;
040    /** Parsing media-type state. */
041    protected static final int S_MEDIATYPE = 2;
042    /** Lenient parsing of trailing whitespace after media-type state. */
043    protected static final int S_MEDIATYPE_WHITESPACE = 3;
044    /** Parsing after a ';' character state. */
045    protected static final int S_SEMICOLON = 4;
046    /** Parsing the parameter name state. */
047    protected static final int S_PARAM_NAME = 5;
048    /** Parsing after a '=' character state. */
049    protected static final int S_PARAM_EQ = 6;
050    /** Parsing a parameter value token state. */
051    protected static final int S_PARAM_VALUE = 7;
052    /** Parsing a quote parameter value state. */
053    protected static final int S_PARAM_QUOTED_VALUE = 8;
054    /** Parsing a quoted pair character state. */
055    protected static final int S_PARAM_QUOTED_PAIR = 9;
056    /** Lenient parsing of trailing whitespace after argument value state. */
057    protected static final int S_PARAM_VALUE_WHITESPACE = 10;
058
059    /** Control character characteristic. */
060    protected static final int CC_CONTROL = 1;
061    /** Separator character characteristic. */
062    protected static final int CC_SEPARATOR_WS = 2;
063
064    /** Parsed Content-type. */
065    public String contentType;
066
067    /** Parsed Media-type. */
068    public String mediaType;
069
070    /** Optional <code>Map</code> of parameters. */
071    public Map<String, String> parameters;
072
073    /** rfc2616 separator minus space and tab. */
074    protected static final String separators = "()<>@,;:\\\"/[]?={} \t";
075
076    /** Table of separator and control characters. */
077    protected static final byte[] charCharacteristicsTab = new byte[256];
078
079    /*
080     * Populate table with separator and control characters.
081     */
082    static {
083        for (int i=0; i<separators.length(); ++i) {
084            charCharacteristicsTab[separators.charAt(i)] = CC_SEPARATOR_WS;
085        }
086        for (int i=0; i<32; ++i) {
087            if (i != '\t') {
088                charCharacteristicsTab[i] = CC_CONTROL;
089            }
090        }
091    }
092
093    /**
094     * Check whether character is a valid token.
095     * @param c character to check
096     * @return boolean indicating whether character is a valid token
097     */
098    public static boolean isTokenCharacter(int c) {
099        return (c >= 0 && c < 256 && charCharacteristicsTab[c] == 0) || c >= 256;
100    }
101
102    /**
103     * Tries to parse and validate the given content-type string. Also tries
104     * to parse and validate any optional parameters present in the string.
105     * @param contentTypeStr content-type string
106     * @return a content-type object with all information in separate
107     * structures.
108     */
109    public static ContentType parseContentType(String contentTypeStr) {
110        if (contentTypeStr == null || contentTypeStr.length() == 0) {
111            return null;
112        }
113        ContentType ct = new ContentType();
114        StringBuffer nameSb = new StringBuffer();
115        StringBuffer valueSb = null;
116        int state = S_START;
117        int idx = 0;
118        int c;
119        boolean bLoop = true;
120        while (bLoop) {
121            if (idx < contentTypeStr.length()) {
122                c = contentTypeStr.charAt(idx);
123            } else {
124                c = -1;
125                bLoop = false;
126            }
127            switch (state) {
128            case S_START:
129                if (c == ' ' || c == '\t') {
130                    ++idx;
131                } else if (isTokenCharacter(c)) {
132                    nameSb.setLength(0);
133                    nameSb.append((char) c);
134                    ++idx;
135                    state = S_CONTENTTYPE;
136                } else {
137                    // /;= (-1) etc.
138                    return null;
139                }
140                break;
141            case S_CONTENTTYPE:
142                if (isTokenCharacter(c)) {
143                    nameSb.append((char) c);
144                    ++idx;
145                } else if (c == '/') {
146                    // Name always at least one.
147                    ct.contentType = nameSb.toString().toLowerCase();
148                    ++idx;
149                    nameSb.setLength(0);
150                    state = S_MEDIATYPE;
151                } else {
152                    // ;= (-1) etc.
153                    return null;
154                }
155                break;
156            case S_MEDIATYPE:
157                if (isTokenCharacter(c)) {
158                    nameSb.append((char) c);
159                    ++idx;
160                } else if (c == -1) {
161                    if (nameSb.length() == 0) {
162                        return null;
163                    }
164                    ct.mediaType = nameSb.toString().toLowerCase();
165                } else if (c == ';') {
166                    if (nameSb.length() == 0) {
167                        return null;
168                    }
169                    ct.mediaType = nameSb.toString().toLowerCase();
170                    ++idx;
171                    valueSb = new StringBuffer();
172                    ct.parameters = new HashMap<String, String>();
173                    state = S_SEMICOLON;
174                } else if (c == ' ' || c == '\t') {
175                    if (nameSb.length() == 0) {
176                        return null;
177                    }
178                    ct.mediaType = nameSb.toString().toLowerCase();
179                    ++idx;
180                    state = S_MEDIATYPE_WHITESPACE;
181                } else {
182                    return null;
183                }
184                break;
185            case S_MEDIATYPE_WHITESPACE:
186                if (c == ' ' || c == '\t') {
187                    ++idx;
188                } else if (c != -1) {
189                    return null;
190                }
191                break;
192            case S_SEMICOLON:
193                if (c == ' ' || c == '\t') {
194                    ++idx;
195                } else if (isTokenCharacter(c)) {
196                    nameSb.setLength(0);
197                    valueSb.setLength(0);
198                    nameSb.append((char) c);
199                    ++idx;
200                    state = S_PARAM_NAME;
201                } else if (c == -1) {
202                    // Allow contenttype/mediatype; and optional parameters
203                } else {
204                    // /;= etc.
205                    return null;
206                }
207                break;
208            case S_PARAM_NAME:
209                if (isTokenCharacter(c)) {
210                    nameSb.append((char) c);
211                    ++idx;
212                } else if (c == '=') {
213                    // Name always at least one.
214                    ++idx;
215                    state = S_PARAM_EQ;
216                } else {
217                    // (-1) etc.
218                    return null;
219                }
220                break;
221            case S_PARAM_EQ:
222                if (isTokenCharacter(c)) {
223                    valueSb.append((char) c);
224                    ++idx;
225                    state = S_PARAM_VALUE;
226                } else if (c == '"') {
227                    ++idx;
228                    state = S_PARAM_QUOTED_VALUE;
229                }
230                else {
231                    // (-1) etc.
232                    return null;
233                }
234                break;
235            case S_PARAM_VALUE:
236                if (isTokenCharacter(c)) {
237                    valueSb.append((char) c);
238                    ++idx;
239                } else if (c == -1) {
240                    ct.parameters.put(nameSb.toString().toLowerCase(),
241                            valueSb.toString());
242                } else if (c == ';') {
243                    ct.parameters.put(nameSb.toString().toLowerCase(),
244                            valueSb.toString());
245                    ++idx;
246                    state = S_SEMICOLON;
247                } else if (c == ' ' || c == '\t') {
248                    ct.parameters.put(nameSb.toString().toLowerCase(),
249                            valueSb.toString());
250                    ++idx;
251                    state = S_PARAM_VALUE_WHITESPACE;
252                } else {
253                    // etc.
254                    return null;
255                }
256                break;
257            case S_PARAM_QUOTED_VALUE:
258                if (c == '"') {
259                    ct.parameters.put(nameSb.toString().toLowerCase(),
260                            valueSb.toString());
261                    ++idx;
262                    state = S_PARAM_VALUE_WHITESPACE;
263                } else if (c == '\\') {
264                    ++idx;
265                    state = S_PARAM_QUOTED_PAIR;
266                } else if (c != -1) {
267                    valueSb.append((char) c);
268                    ++idx;
269                } else {
270                    // (-1)
271                    return null;
272                }
273                break;
274            case S_PARAM_QUOTED_PAIR:
275                if (c != -1) {
276                    valueSb.append((char) c);
277                    ++idx;
278                    state = S_PARAM_QUOTED_VALUE;
279                } else {
280                    // (-1)
281                    return null;
282                }
283                break;
284            case S_PARAM_VALUE_WHITESPACE:
285                if (c == ' ' || c == '\t') {
286                    ++idx;
287                } else if (c == ';') {
288                    ++idx;
289                    state = S_SEMICOLON;
290                } else if (c != -1) {
291                    return null;
292                }
293                break;
294            }
295        }
296        return ct;
297    }
298
299    /**
300     * Return parameter value associated with supplied parameter name.
301     * @param name parameter name
302     * @return parameter value or null
303     */
304    public String getParameter(String name) {
305        if (name == null || name.length() == 0 || parameters == null) {
306            return null;
307        }
308        return parameters.get(name.toLowerCase());
309    }
310
311    /**
312     * Set a parameter with the given (name, value) pair.
313     * @param name parameter name
314     * @param value parameter value
315     */
316    public void setParameter(String name, String value) {
317        if (name != null && name.length() > 0 && value != null) {
318            if (parameters == null) {
319                parameters = new HashMap<String, String>();
320            }
321            parameters.put(name.toLowerCase(), value);
322        }
323    }
324
325    /**
326     * Determines whether a string should be quoted by examining if the string
327     * contains tabs and/or spaces.
328     * @param str input string
329     * @return boolean indicating if the string should be quoted
330     */
331    public static boolean quote(String str) {
332        boolean quote = false;
333        if (str != null && str.length() > 0) {
334            int idx = 0;
335            char c;
336            while (idx<str.length() && !quote) {
337                c = str.charAt(idx++);
338                if (c < 256 && (charCharacteristicsTab[c] & CC_SEPARATOR_WS) != 0) {
339                    quote = true;
340                }
341            }
342        }
343        return quote;
344    }
345
346    /**
347     * Returns the content-type omitting any parameters present.
348     * @return content-type without any parameters included
349     */
350    public String toStringShort() {
351        StringBuffer sb = new StringBuffer();
352        sb.append(contentType);
353        sb.append('/');
354        sb.append(mediaType);
355        return sb.toString();
356    }
357
358    @Override
359    public String toString() {
360        StringBuffer sb = new StringBuffer();
361        sb.append(contentType);
362        sb.append('/');
363        sb.append(mediaType);
364        if (parameters != null) {
365            Iterator<Entry<String, String>> iter = parameters.entrySet().iterator();
366            Entry<String, String> entry;
367            while (iter.hasNext()) {
368                sb.append("; ");
369                entry = iter.next();
370                sb.append(entry.getKey());
371                sb.append('=');
372                if (quote(entry.getValue())) {
373                    sb.append('"');
374                    sb.append(entry.getValue());
375                    sb.append('"');
376                }
377                else {
378                    sb.append(entry.getValue());
379                }
380            }
381        }
382        return sb.toString();
383    }
384
385    @Override
386    public boolean equals(Object obj) {
387        if (obj == null || !(obj instanceof ContentType)) {
388            return false;
389        }
390        ContentType contentTypeObj = (ContentType)obj;
391        if (contentType != null) {
392            if (!contentType.equals(contentTypeObj.contentType)) {
393                return false;
394            }
395        } else if (contentTypeObj.contentType != null) {
396            return false;
397        }
398        if (mediaType != null) {
399            if (!mediaType.equals(contentTypeObj.mediaType)) {
400                return false;
401            }
402        } else if (contentTypeObj.mediaType != null) {
403            return false;
404        }
405        if (parameters != null) {
406            if (contentTypeObj.parameters == null) {
407                return false;
408            }
409            if (parameters.size() != contentTypeObj.parameters.size()) {
410                return false;
411            }
412            if (parameters.size() > 0) {
413                Iterator<Entry<String, String>> iter = parameters.entrySet().iterator();
414                Entry<String, String> entry;
415                String value;
416                while (iter.hasNext()) {
417                    entry = iter.next();
418                    if (!contentTypeObj.parameters.containsKey(entry.getKey())) {
419                        return false;
420                    }
421                    value = entry.getValue();
422                    if (value != null) {
423                        if (!value.equals(contentTypeObj.parameters.get(entry.getKey()))) {
424                            return false;
425                        }
426                    } else if (contentTypeObj.parameters.get(entry.getKey()) != null) {
427                        return false;
428                    }
429                }
430            }
431        } else if (contentTypeObj.parameters != null) {
432            return false;
433        }
434        return true;
435    }
436
437    @Override
438    public int hashCode() {
439        int hashCode = 0;
440        if (contentType != null) {
441            hashCode ^= contentType.hashCode();
442        }
443        if (mediaType != null) {
444            hashCode ^= mediaType.hashCode();
445        }
446        if (parameters != null) {
447            // Ensure null and empty parameters map does not mean object equality!
448            hashCode ^= 31331;
449            Iterator<Entry<String, String>> iter = parameters.entrySet().iterator();
450            Entry<String, String> entry;
451            String value;
452            while (iter.hasNext()) {
453                entry = iter.next();
454                hashCode ^= entry.getKey().hashCode();
455                value = entry.getValue();
456                if (value != null) {
457                    hashCode ^= value.hashCode();
458                }
459            }
460        }
461        return hashCode;
462    }
463
464}