View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.james.jdkim.tagvalue;
21  
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.regex.Pattern;
30  
31  /**
32   * This class handle a tag=value list string as defined by DKIM specification It
33   * also supports mandatoryTags and default values as a commodity to subclasses.
34   */
35  public class TagValue {
36  
37      private static final boolean DEBUG = false;
38      protected static final boolean VALIDATION = true;
39  
40      private static Pattern tagPattern = Pattern
41              .compile("^[A-Za-z][A-Za-z0-9_]*$");
42      private static final String tval = "[^; \t\r\n]+";
43      // validate value chars
44      private static Pattern valuePattern = Pattern.compile("^(" + tval
45              + "((\r\n[\t ]|[\t ])+" + tval + ")*)?$");
46  
47      // we may use a TreeMap because we may need to know original order.
48      private Map/* String, CharSequence */tagValues;
49  
50      protected Set/* String */mandatoryTags = new HashSet();
51      protected Map/* String, CharSequence */defaults = new HashMap();
52      private String stringRepresentation = null;
53  
54      protected Set tagSet() {
55          return tagValues.keySet();
56      }
57      protected boolean containsTag(String tag) {
58          return tagValues.containsKey(tag);
59      }
60      
61      protected CharSequence trimFWS(CharSequence data, int tStart, int tStop,
62              boolean trimWSP) {
63          if (DEBUG)
64              System.out.println("1[" + data + "]" + tStart + "|" + tStop + "="
65                      + data.subSequence(tStart, tStop + 1) + "]");
66          // rimozione di FWS a inizio selezione
67          while (tStart < tStop
68                  && (data.charAt(tStart) == ' ' || data.charAt(tStart) == '\t')
69                  || (tStart < tStop - 2 && data.charAt(tStart) == '\r'
70                          && data.charAt(tStart + 1) == '\n' && (data
71                          .charAt(tStart + 2) == ' ' || data.charAt(tStart + 2) == '\t'))) {
72              if (data.charAt(tStart) == '\r')
73                  tStart += 3;
74              else
75                  tStart++;
76          }
77  
78          if (DEBUG)
79              System.out.println("2[" + data + "]" + tStart + "|" + tStop + "="
80                      + data.subSequence(tStart, tStop + 1) + "]");
81          // rimozione di FWS a fine selezione.
82          while (tStart < tStop
83                  && (data.charAt(tStop) == ' ' || data.charAt(tStop) == '\t')) {
84              tStop--;
85              if ((tStart <= tStop - 1 && data.charAt(tStop) == '\n' && data
86                      .charAt(tStop - 1) == '\r')
87                      || (tStart < tStop && (data.charAt(tStop) == ' ' || data
88                              .charAt(tStop) == '\t'))) {
89                  if (data.charAt(tStop) == '\n')
90                      tStop -= 2;
91                  else
92                      tStop--;
93              }
94          }
95  
96          if (DEBUG)
97              System.out.println("3[" + data + "]" + tStart + "|" + tStop + "="
98                      + data.subSequence(tStart, tStop + 1) + "]");
99          if (trimWSP) {
100             return trimWSP(data, tStart, tStop);
101         } else {
102             return data.subSequence(tStart, tStop + 1);
103         }
104     }
105 
106     private CharSequence trimWSP(CharSequence data, int vStart, int vStop) {
107         if (vStop < vStart - 1)
108             throw new IllegalArgumentException("Stop must be >= than start");
109         while (vStart <= vStop
110                 && (data.charAt(vStart) == ' ' || data.charAt(vStart) == '\t'))
111             vStart++;
112         while (vStart <= vStop
113                 && (data.charAt(vStop) == ' ' || data.charAt(vStop) == '\t'))
114             vStop--;
115         return data.subSequence(vStart, vStop + 1);
116     }
117 
118     public TagValue(String data) {
119         tagValues = newTagValue();
120         init();
121         parse(data);
122     }
123 
124     protected Map newTagValue() {
125         // extensions may override this to use TreeMaps in order to keep track
126         // of orders
127         return new HashMap();
128     }
129 
130     protected void init() {
131     }
132 
133     /**
134      * subclasses have to make sure tagValues is initialized during init().
135      * 
136      * @param data
137      *                the string to be parsed
138      */
139     protected void parse(String data) {
140         for (int i = 0; i < data.length(); i++) {
141             int equal = data.indexOf('=', i);
142             if (equal == -1) {
143                 // TODO check whether this is correct or not
144                 // this allow FWS/WSP after the final ";"
145                 String rest = data.substring(i);
146                 if (rest.length() > 0
147                         && trimFWS(rest, 0, rest.length() - 1, true).length() > 0) {
148                     throw new IllegalStateException(
149                             "Unexpected termination at position " + i + ": "
150                                     + data);
151                 }
152                 i = data.length();
153                 continue;
154             }
155             // we could start from "equals" but we start from "i" in
156             // order to spot invalid values before validation.
157             int next = data.indexOf(';', i);
158             if (next == -1) {
159                 next = data.length();
160             }
161 
162             if (equal > next) {
163                 throw new IllegalStateException("Found ';' before '=' in "
164                         + data);
165             }
166 
167             CharSequence tag = trimFWS(data, i, equal - 1, true).toString();
168             if (VALIDATION && !tagPattern.matcher(tag).matches()) {
169                 throw new IllegalStateException("Syntax error in tag: " + tag);
170             }
171             String tagString = tag.toString();
172             if (tagValues.containsKey(tagString)) {
173                 throw new IllegalStateException(
174                         "Syntax error (duplicate tag): " + tag);
175             }
176 
177             CharSequence value = trimFWS(data, equal + 1, next - 1, true);
178             if (VALIDATION && !valuePattern.matcher(value).matches()) {
179                 throw new IllegalStateException("Syntax error in value: "
180                         + value);
181             }
182 
183             tagValues.put(tagString, value);
184             i = next;
185         }
186         this.stringRepresentation  = data;
187     }
188 
189     public int hashCode() {
190         final int prime = 31;
191         int result = 1;
192         result = prime * result
193                 + ((tagValues == null) ? 0 : tagValues.hashCode());
194         return result;
195     }
196 
197     public boolean equals(Object obj) {
198         if (this == obj)
199             return true;
200         if (obj == null)
201             return false;
202         if (getClass() != obj.getClass())
203             return false;
204         TagValue other = (TagValue) obj;
205         if (tagValues == null) {
206             if (other.tagValues != null)
207                 return false;
208         } else if (!tagValues.equals(other.tagValues))
209             return false;
210         return true;
211     }
212 
213     public Set getTags() {
214         return tagValues.keySet();
215     }
216 
217     protected CharSequence getValue(String key) {
218         CharSequence val = (CharSequence) tagValues.get(key);
219         if (val == null)
220             return getDefault(key);
221         else
222             return val;
223     }
224     
225     protected void setValue(String tag, String value) {
226         stringRepresentation = null;
227         tagValues.put(tag, value);
228     }
229 
230 
231     protected CharSequence getDefault(String key) {
232         return (CharSequence) defaults.get(key);
233     }
234 
235     public void validate() {
236         // check mandatory fields
237         for (Iterator i = mandatoryTags.iterator(); i.hasNext();) {
238             String tag = (String) i.next();
239             if (getValue(tag) == null)
240                 throw new IllegalStateException("Missing mandatory tag: " + tag);
241         }
242     }
243 
244     protected List stringToColonSeparatedList(String h, Pattern pattern) {
245         List headers = new ArrayList();
246         for (int i = 0; i < h.length(); i++) {
247             int p = h.indexOf(':', i);
248             if (p == -1)
249                 p = h.length();
250             CharSequence cs = trimFWS(h, i, p - 1, false);
251             if (VALIDATION) {
252                 if (!pattern.matcher(cs).matches())
253                     throw new IllegalStateException(
254                             "Syntax error in field name: " + cs);
255             }
256             headers.add(cs);
257             i = p;
258         }
259         return headers;
260     }
261 
262     protected boolean isInListCaseInsensitive(CharSequence hash, List hashes) {
263         for (Iterator i = hashes.iterator(); i.hasNext();) {
264             CharSequence suppHash = (CharSequence) i.next();
265             if (hash.toString().equalsIgnoreCase(suppHash.toString()))
266                 return true;
267         }
268         return false;
269     }
270 
271     public String toString() {
272         if (stringRepresentation == null) {
273             updateStringRepresentation();
274         }
275         return stringRepresentation;
276     }
277     
278     private void updateStringRepresentation() {
279         // calculate a new string representation
280         StringBuffer res = new StringBuffer();
281         Set s = getTags();
282         for (Iterator i = s.iterator(); i.hasNext();) {
283             String tag = (String) i.next();
284             res.append(tag);
285             res.append("=");
286             res.append(getValue(tag));
287             res.append("; ");
288         }
289         // TODO add folding
290         stringRepresentation = res.toString();
291     }
292 
293 }