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 | } |