1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.server.web; |
7 | |
8 | import java.text.ParseException; |
9 | import java.util.HashMap; |
10 | import java.util.List; |
11 | import java.util.Map; |
12 | import org.h2.util.New; |
13 | |
14 | /** |
15 | * A page parser can parse an HTML page and replace the tags there. |
16 | * This class is used by the H2 Console. |
17 | */ |
18 | public class PageParser { |
19 | private static final int TAB_WIDTH = 4; |
20 | |
21 | private final String page; |
22 | private int pos; |
23 | private final Map<String, Object> settings; |
24 | private final int len; |
25 | private StringBuilder result; |
26 | |
27 | private PageParser(String page, Map<String, Object> settings, int pos) { |
28 | this.page = page; |
29 | this.pos = pos; |
30 | this.len = page.length(); |
31 | this.settings = settings; |
32 | result = new StringBuilder(len); |
33 | } |
34 | |
35 | /** |
36 | * Replace the tags in the HTML page with the given settings. |
37 | * |
38 | * @param page the HTML page |
39 | * @param settings the settings |
40 | * @return the converted page |
41 | */ |
42 | public static String parse(String page, Map<String, Object> settings) { |
43 | PageParser block = new PageParser(page, settings, 0); |
44 | return block.replaceTags(); |
45 | } |
46 | |
47 | private void setError(int i) { |
48 | String s = page.substring(0, i) + "####BUG####" + page.substring(i); |
49 | s = PageParser.escapeHtml(s); |
50 | result = new StringBuilder(); |
51 | result.append(s); |
52 | } |
53 | |
54 | private String parseBlockUntil(String end) throws ParseException { |
55 | PageParser block = new PageParser(page, settings, pos); |
56 | block.parseAll(); |
57 | if (!block.readIf(end)) { |
58 | throw new ParseException(page, block.pos); |
59 | } |
60 | pos = block.pos; |
61 | return block.result.toString(); |
62 | } |
63 | |
64 | private String replaceTags() { |
65 | try { |
66 | parseAll(); |
67 | if (pos != len) { |
68 | setError(pos); |
69 | } |
70 | } catch (ParseException e) { |
71 | setError(pos); |
72 | } |
73 | return result.toString(); |
74 | } |
75 | |
76 | @SuppressWarnings("unchecked") |
77 | private void parseAll() throws ParseException { |
78 | StringBuilder buff = result; |
79 | String p = page; |
80 | int i = pos; |
81 | for (; i < len; i++) { |
82 | char c = p.charAt(i); |
83 | switch (c) { |
84 | case '<': { |
85 | if (p.charAt(i + 3) == ':' && p.charAt(i + 1) == '/') { |
86 | // end tag |
87 | pos = i; |
88 | return; |
89 | } else if (p.charAt(i + 2) == ':') { |
90 | pos = i; |
91 | if (readIf("<c:forEach")) { |
92 | String var = readParam("var"); |
93 | String items = readParam("items"); |
94 | read(">"); |
95 | int start = pos; |
96 | List<Object> list = (List<Object>) get(items); |
97 | if (list == null) { |
98 | result.append("?items?"); |
99 | list = New.arrayList(); |
100 | } |
101 | if (list.size() == 0) { |
102 | parseBlockUntil("</c:forEach>"); |
103 | } |
104 | for (Object o : list) { |
105 | settings.put(var, o); |
106 | pos = start; |
107 | String block = parseBlockUntil("</c:forEach>"); |
108 | result.append(block); |
109 | } |
110 | } else if (readIf("<c:if")) { |
111 | String test = readParam("test"); |
112 | int eq = test.indexOf("=='"); |
113 | if (eq < 0) { |
114 | setError(i); |
115 | return; |
116 | } |
117 | String val = test.substring(eq + 3, test.length() - 1); |
118 | test = test.substring(0, eq); |
119 | String value = (String) get(test); |
120 | read(">"); |
121 | String block = parseBlockUntil("</c:if>"); |
122 | pos--; |
123 | if (value.equals(val)) { |
124 | result.append(block); |
125 | } |
126 | } else { |
127 | setError(i); |
128 | return; |
129 | } |
130 | i = pos; |
131 | } else { |
132 | buff.append(c); |
133 | } |
134 | break; |
135 | } |
136 | case '$': |
137 | if (p.length() > i + 1 && p.charAt(i + 1) == '{') { |
138 | i += 2; |
139 | int j = p.indexOf('}', i); |
140 | if (j < 0) { |
141 | setError(i); |
142 | return; |
143 | } |
144 | String item = p.substring(i, j).trim(); |
145 | i = j; |
146 | String s = (String) get(item); |
147 | replaceTags(s); |
148 | } else { |
149 | buff.append(c); |
150 | } |
151 | break; |
152 | default: |
153 | buff.append(c); |
154 | break; |
155 | } |
156 | } |
157 | pos = i; |
158 | } |
159 | |
160 | @SuppressWarnings("unchecked") |
161 | private Object get(String item) { |
162 | int dot = item.indexOf('.'); |
163 | if (dot >= 0) { |
164 | String sub = item.substring(dot + 1); |
165 | item = item.substring(0, dot); |
166 | HashMap<String, Object> map = (HashMap<String, Object>) settings.get(item); |
167 | if (map == null) { |
168 | return "?" + item + "?"; |
169 | } |
170 | return map.get(sub); |
171 | } |
172 | return settings.get(item); |
173 | } |
174 | |
175 | private void replaceTags(String s) { |
176 | if (s != null) { |
177 | result.append(PageParser.parse(s, settings)); |
178 | } |
179 | } |
180 | |
181 | private String readParam(String name) throws ParseException { |
182 | read(name); |
183 | read("="); |
184 | read("\""); |
185 | int start = pos; |
186 | while (page.charAt(pos) != '"') { |
187 | pos++; |
188 | } |
189 | int end = pos; |
190 | read("\""); |
191 | String s = page.substring(start, end); |
192 | return PageParser.parse(s, settings); |
193 | } |
194 | |
195 | private void skipSpaces() { |
196 | while (page.charAt(pos) == ' ') { |
197 | pos++; |
198 | } |
199 | } |
200 | |
201 | private void read(String s) throws ParseException { |
202 | if (!readIf(s)) { |
203 | throw new ParseException(s, pos); |
204 | } |
205 | } |
206 | |
207 | private boolean readIf(String s) { |
208 | skipSpaces(); |
209 | if (page.regionMatches(pos, s, 0, s.length())) { |
210 | pos += s.length(); |
211 | skipSpaces(); |
212 | return true; |
213 | } |
214 | return false; |
215 | } |
216 | |
217 | /** |
218 | * Convert data to HTML, but don't convert newlines and multiple spaces. |
219 | * |
220 | * @param s the data |
221 | * @return the escaped html text |
222 | */ |
223 | static String escapeHtmlData(String s) { |
224 | return escapeHtml(s, false); |
225 | } |
226 | |
227 | /** |
228 | * Convert data to HTML, including newlines and multiple spaces. |
229 | * |
230 | * @param s the data |
231 | * @return the escaped html text |
232 | */ |
233 | public static String escapeHtml(String s) { |
234 | return escapeHtml(s, true); |
235 | } |
236 | |
237 | private static String escapeHtml(String s, boolean convertBreakAndSpace) { |
238 | if (s == null) { |
239 | return null; |
240 | } |
241 | if (convertBreakAndSpace) { |
242 | if (s.length() == 0) { |
243 | return " "; |
244 | } |
245 | } |
246 | StringBuilder buff = new StringBuilder(s.length()); |
247 | boolean convertSpace = true; |
248 | for (int i = 0; i < s.length(); i++) { |
249 | char c = s.charAt(i); |
250 | if (c == ' ' || c == '\t') { |
251 | // convert tabs into spaces |
252 | for (int j = 0; j < (c == ' ' ? 1 : TAB_WIDTH); j++) { |
253 | if (convertSpace && convertBreakAndSpace) { |
254 | buff.append(" "); |
255 | } else { |
256 | buff.append(' '); |
257 | convertSpace = true; |
258 | } |
259 | } |
260 | continue; |
261 | } |
262 | convertSpace = false; |
263 | switch (c) { |
264 | case '$': |
265 | // so that ${ } in the text is interpreted correctly |
266 | buff.append("$"); |
267 | break; |
268 | case '<': |
269 | buff.append("<"); |
270 | break; |
271 | case '>': |
272 | buff.append(">"); |
273 | break; |
274 | case '&': |
275 | buff.append("&"); |
276 | break; |
277 | case '"': |
278 | buff.append("""); |
279 | break; |
280 | case '\'': |
281 | buff.append("'"); |
282 | break; |
283 | case '\n': |
284 | if (convertBreakAndSpace) { |
285 | buff.append("<br />"); |
286 | convertSpace = true; |
287 | } else { |
288 | buff.append(c); |
289 | } |
290 | break; |
291 | default: |
292 | if (c >= 128) { |
293 | buff.append("&#").append((int) c).append(';'); |
294 | } else { |
295 | buff.append(c); |
296 | } |
297 | break; |
298 | } |
299 | } |
300 | return buff.toString(); |
301 | } |
302 | |
303 | /** |
304 | * Escape text as a the javascript string. |
305 | * |
306 | * @param s the text |
307 | * @return the javascript string |
308 | */ |
309 | static String escapeJavaScript(String s) { |
310 | if (s == null) { |
311 | return null; |
312 | } |
313 | if (s.length() == 0) { |
314 | return ""; |
315 | } |
316 | StringBuilder buff = new StringBuilder(s.length()); |
317 | for (int i = 0; i < s.length(); i++) { |
318 | char c = s.charAt(i); |
319 | switch (c) { |
320 | case '"': |
321 | buff.append("\\\""); |
322 | break; |
323 | case '\'': |
324 | buff.append("\\'"); |
325 | break; |
326 | case '\\': |
327 | buff.append("\\\\"); |
328 | break; |
329 | case '\n': |
330 | buff.append("\\n"); |
331 | break; |
332 | case '\r': |
333 | buff.append("\\r"); |
334 | break; |
335 | case '\t': |
336 | buff.append("\\t"); |
337 | break; |
338 | default: |
339 | buff.append(c); |
340 | break; |
341 | } |
342 | } |
343 | return buff.toString(); |
344 | } |
345 | } |