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