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