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