001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.GridBagLayout; 008import java.util.Arrays; 009import java.util.HashMap; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014 015import javax.swing.JLabel; 016import javax.swing.JOptionPane; 017import javax.swing.JPanel; 018 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.gui.ExtendedDialog; 021import org.openstreetmap.josm.gui.help.HelpUtil; 022import org.openstreetmap.josm.gui.widgets.UrlLabel; 023import org.openstreetmap.josm.io.XmlWriter; 024import org.openstreetmap.josm.tools.LanguageInfo.LocaleType; 025 026/** 027 * Class that helps to parse tags from arbitrary text 028 */ 029public final class TextTagParser { 030 031 // properties need JOSM restart to apply, modified rarely enough 032 private static final int MAX_KEY_LENGTH = Main.pref.getInteger("tags.paste.max-key-length", 50); 033 private static final int MAX_KEY_COUNT = Main.pref.getInteger("tags.paste.max-key-count", 30); 034 private static final String KEY_PATTERN = Main.pref.get("tags.paste.tag-pattern", "[0-9a-zA-Z:_]*"); 035 private static final int MAX_VALUE_LENGTH = 255; 036 037 private TextTagParser() { 038 // Hide default constructor for utils classes 039 } 040 041 public static class TextAnalyzer { 042 private boolean quotesStarted; 043 private boolean esc; 044 private StringBuilder s = new StringBuilder(200); 045 private int pos; 046 private String data; 047 private int n; 048 049 public TextAnalyzer(String text) { 050 pos = 0; 051 data = text; 052 n = data.length(); 053 } 054 055 /** 056 * Read tags from "Free format" 057 * @return map of tags 058 */ 059 private Map<String, String> getFreeParsedTags() { 060 String k, v; 061 Map<String, String> tags = new HashMap<>(); 062 063 while (true) { 064 skipEmpty(); 065 if (pos == n) { 066 break; 067 } 068 k = parseString("\n\r\t= "); 069 if (pos == n) { 070 tags.clear(); 071 break; 072 } 073 skipSign(); 074 if (pos == n) { 075 tags.clear(); 076 break; 077 } 078 v = parseString("\n\r\t "); 079 tags.put(k, v); 080 } 081 return tags; 082 } 083 084 private String parseString(String stopChars) { 085 char[] stop = stopChars.toCharArray(); 086 Arrays.sort(stop); 087 char c; 088 while (pos < n) { 089 c = data.charAt(pos); 090 if (esc) { 091 esc = false; 092 s.append(c); // \" \\ 093 } else if (c == '\\') { 094 esc = true; 095 } else if (c == '\"' && !quotesStarted) { // opening " 096 if (!s.toString().trim().isEmpty()) { // we had ||some text"|| 097 s.append(c); // just add ", not open 098 } else { 099 s.delete(0, s.length()); // forget that empty characthers and start reading ".... 100 quotesStarted = true; 101 } 102 } else if (c == '\"' && quotesStarted) { // closing " 103 quotesStarted = false; 104 pos++; 105 break; 106 } else if (!quotesStarted && (Arrays.binarySearch(stop, c) >= 0)) { 107 // stop-symbol found 108 pos++; 109 break; 110 } else { 111 // skip non-printable characters 112 if (c >= 32) s.append(c); 113 } 114 pos++; 115 } 116 117 String res = s.toString(); 118 s.delete(0, s.length()); 119 return res.trim(); 120 } 121 122 private void skipSign() { 123 char c; 124 boolean signFound = false; 125 while (pos < n) { 126 c = data.charAt(pos); 127 if (c == '\t' || c == '\n' || c == ' ') { 128 pos++; 129 } else if (c == '=') { 130 if (signFound) break; // a = =qwerty means "a"="=qwerty" 131 signFound = true; 132 pos++; 133 } else { 134 break; 135 } 136 } 137 } 138 139 private void skipEmpty() { 140 char c; 141 while (pos < n) { 142 c = data.charAt(pos); 143 if (c == '\t' || c == '\n' || c == '\r' || c == ' ') { 144 pos++; 145 } else { 146 break; 147 } 148 } 149 } 150 } 151 152 protected static String unescape(String k) { 153 if (!(k.startsWith("\"") && k.endsWith("\""))) { 154 if (k.contains("=")) { 155 // '=' not in quotes will be treated as an error! 156 return null; 157 } else { 158 return k; 159 } 160 } 161 String text = k.substring(1, k.length()-1); 162 return (new TextAnalyzer(text)).parseString("\r\t\n"); 163 } 164 165 /** 166 * Try to find tag-value pairs in given text 167 * @param text - text in which tags are looked for 168 * @param splitRegex - text is splitted into parts with this delimiter 169 * @param tagRegex - each part is matched against this regex 170 * @param unescapeTextInQuotes - if true, matched tag and value will be analyzed more thoroughly 171 * @return map of tags 172 */ 173 public static Map<String, String> readTagsByRegexp(String text, String splitRegex, String tagRegex, boolean unescapeTextInQuotes) { 174 String[] lines = text.split(splitRegex); 175 Pattern p = Pattern.compile(tagRegex); 176 Map<String, String> tags = new HashMap<>(); 177 String k = null, v = null; 178 for (String line: lines) { 179 if (line.trim().isEmpty()) continue; // skip empty lines 180 Matcher m = p.matcher(line); 181 if (m.matches()) { 182 k = m.group(1).trim(); 183 v = m.group(2).trim(); 184 if (unescapeTextInQuotes) { 185 k = unescape(k); 186 v = unescape(v); 187 if (k == null || v == null) return null; 188 } 189 tags.put(k, v); 190 } else { 191 return null; 192 } 193 } 194 if (!tags.isEmpty()) { 195 return tags; 196 } else { 197 return null; 198 } 199 } 200 201 public static Map<String, String> getValidatedTagsFromText(String buf) { 202 Map<String, String> tags = readTagsFromText(buf); 203 return validateTags(tags) ? tags : null; 204 } 205 206 /** 207 * Apply different methods to extract tag-value pairs from arbitrary text 208 * @param buf buffer 209 * @return null if no format is suitable 210 */ 211 public static Map<String, String> readTagsFromText(String buf) { 212 Map<String, String> tags; 213 214 // Format 215 // tag1\tval1\ntag2\tval2\n 216 tags = readTagsByRegexp(buf, "[\\r\\n]+", ".*?([a-zA-Z0-9:_]+).*\\t(.*?)", false); 217 // try "tag\tvalue\n" format 218 if (tags != null) return tags; 219 220 // Format 221 // a=b \n c=d \n "a b"=hello 222 // SORRY: "a=b" = c is not supported fror now, only first = will be considered 223 // a = "b=c" is OK 224 // a = b=c - this method of parsing fails intentionally 225 tags = readTagsByRegexp(buf, "[\\n\\t\\r]+", "(.*?)=(.*?)", true); 226 // try format t1=v1\n t2=v2\n ... 227 if (tags != null) return tags; 228 229 // JSON-format 230 String bufJson = buf.trim(); 231 // trim { }, if there are any 232 if (bufJson.startsWith("{") && bufJson.endsWith("}")) 233 bufJson = bufJson.substring(1, bufJson.length()-1); 234 tags = readTagsByRegexp(bufJson, "[\\s]*,[\\s]*", 235 "[\\s]*(\\\".*?[^\\\\]\\\")"+"[\\s]*:[\\s]*"+"(\\\".*?[^\\\\]\\\")[\\s]*", true); 236 if (tags != null) return tags; 237 238 // Free format 239 // a 1 "b" 2 c=3 d 4 e "5" 240 return new TextAnalyzer(buf).getFreeParsedTags(); 241 } 242 243 /** 244 * Check tags for correctness and display warnings if needed 245 * @param tags - map key->value to check 246 * @return true if the tags should be pasted 247 */ 248 public static boolean validateTags(Map<String, String> tags) { 249 int r; 250 int s = tags.size(); 251 if (s > MAX_KEY_COUNT) { 252 // Use trn() even if for english it makes no sense, as s > 30 253 r = warning(trn("There was {0} tag found in the buffer, it is suspicious!", 254 "There were {0} tags found in the buffer, it is suspicious!", s, 255 s), "", "tags.paste.toomanytags"); 256 if (r == 2 || r == 3) return false; if (r == 4) return true; 257 } 258 for (Entry<String, String> entry : tags.entrySet()) { 259 String key = entry.getKey(); 260 String value = entry.getValue(); 261 if (key.length() > MAX_KEY_LENGTH) { 262 r = warning(tr("Key is too long (max {0} characters):", MAX_KEY_LENGTH), key+'='+value, "tags.paste.keytoolong"); 263 if (r == 2 || r == 3) return false; if (r == 4) return true; 264 } 265 if (!key.matches(KEY_PATTERN)) { 266 r = warning(tr("Suspicious characters in key:"), key, "tags.paste.keydoesnotmatch"); 267 if (r == 2 || r == 3) return false; if (r == 4) return true; 268 } 269 if (value.length() > MAX_VALUE_LENGTH) { 270 r = warning(tr("Value is too long (max {0} characters):", MAX_VALUE_LENGTH), value, "tags.paste.valuetoolong"); 271 if (r == 2 || r == 3) return false; if (r == 4) return true; 272 } 273 } 274 return true; 275 } 276 277 private static int warning(String text, String data, String code) { 278 ExtendedDialog ed = new ExtendedDialog( 279 Main.parent, 280 tr("Do you want to paste these tags?"), 281 new String[]{tr("Ok"), tr("Cancel"), tr("Clear buffer"), tr("Ignore warnings")}); 282 ed.setButtonIcons(new String[]{"ok", "cancel", "dialogs/delete", "pastetags"}); 283 ed.setContent("<html><b>"+text + "</b><br/><br/><div width=\"300px\">"+XmlWriter.encode(data, true)+"</html>"); 284 ed.setDefaultButton(2); 285 ed.setCancelButton(2); 286 ed.setIcon(JOptionPane.WARNING_MESSAGE); 287 ed.toggleEnable(code); 288 ed.showDialog(); 289 int r = ed.getValue(); 290 if (r == 0) r = 2; 291 // clean clipboard if user asked 292 if (r == 3) Utils.copyToClipboard(""); 293 return r; 294 } 295 296 /** 297 * Shows message that the buffer can not be pasted, allowing user to clean the buffer 298 * @param helpTopic the help topic of the parent action 299 * TODO: Replace by proper HelpAwareOptionPane instead of self-made help link 300 */ 301 public static void showBadBufferMessage(String helpTopic) { 302 String msg = tr("<html><p> Sorry, it is impossible to paste tags from buffer. It does not contain any JOSM object" 303 + " or suitable text. </p></html>"); 304 JPanel p = new JPanel(new GridBagLayout()); 305 p.add(new JLabel(msg), GBC.eop()); 306 String helpUrl = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(helpTopic, LocaleType.DEFAULT)); 307 if (helpUrl != null) { 308 p.add(new UrlLabel(helpUrl), GBC.eop()); 309 } 310 311 ExtendedDialog ed = new ExtendedDialog( 312 Main.parent, 313 tr("Warning"), 314 new String[]{tr("Ok"), tr("Clear buffer")}); 315 316 ed.setButtonIcons(new String[]{"ok", "dialogs/delete"}); 317 318 ed.setContent(p); 319 ed.setDefaultButton(1); 320 ed.setCancelButton(1); 321 ed.setIcon(JOptionPane.WARNING_MESSAGE); 322 ed.toggleEnable("tags.paste.cleanbadbuffer"); 323 ed.showDialog(); 324 325 int r = ed.getValue(); 326 // clean clipboard if user asked 327 if (r == 2) Utils.copyToClipboard(""); 328 } 329}