summaryrefslogtreecommitdiffstats
path: root/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
blob: 9fe3267e7b767473e46c7d5f1fa3505f3a953f13 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gwtexpui.safehtml.client;

import com.google.gwt.core.client.GWT;

/**
 * Safely constructs a {@link SafeHtml}, escaping user provided content.
 */
public class SafeHtmlBuilder extends SafeHtml {
  private static final Impl impl;

  static {
    if (GWT.isClient()) {
      impl = new ClientImpl();
    } else {
      impl = new ServerImpl();
    }
  }

  private final BufferDirect dBuf;
  private Buffer cb;

  private BufferSealElement sBuf;
  private AttMap att;

  public SafeHtmlBuilder() {
    cb = dBuf = new BufferDirect();
  }

  /** @return true if this builder has not had an append occur yet. */
  public boolean isEmpty() {
    return dBuf.isEmpty();
  }

  /** @return true if this builder has content appended into it. */
  public boolean hasContent() {
    return !isEmpty();
  }

  public SafeHtmlBuilder append(final boolean in) {
    cb.append(in);
    return this;
  }

  public SafeHtmlBuilder append(final char in) {
    switch (in) {
      case '&':
        cb.append("&");
        break;

      case '>':
        cb.append(">");
        break;

      case '<':
        cb.append("&lt;");
        break;

      case '"':
        cb.append("&quot;");
        break;

      case '\'':
        cb.append("&#39;");
        break;

      default:
        cb.append(in);
        break;
    }
    return this;
  }

  public SafeHtmlBuilder append(final int in) {
    cb.append(in);
    return this;
  }

  public SafeHtmlBuilder append(final long in) {
    cb.append(in);
    return this;
  }

  public SafeHtmlBuilder append(final float in) {
    cb.append(in);
    return this;
  }

  public SafeHtmlBuilder append(final double in) {
    cb.append(in);
    return this;
  }

  /** Append already safe HTML as-is, avoiding double escaping. */
  public SafeHtmlBuilder append(final SafeHtml in) {
    if (in != null) {
      cb.append(in.asString());
    }
    return this;
  }

  /** Append the string, escaping unsafe characters. */
  public SafeHtmlBuilder append(final String in) {
    if (in != null) {
      impl.escapeStr(this, in);
    }
    return this;
  }

  /** Append the string, escaping unsafe characters. */
  public SafeHtmlBuilder append(final StringBuilder in) {
    if (in != null) {
      append(in.toString());
    }
    return this;
  }

  /** Append the string, escaping unsafe characters. */
  public SafeHtmlBuilder append(final StringBuffer in) {
    if (in != null) {
      append(in.toString());
    }
    return this;
  }

  /** Append the result of toString(), escaping unsafe characters. */
  public SafeHtmlBuilder append(final Object in) {
    if (in != null) {
      append(in.toString());
    }
    return this;
  }

  /** Append the string, escaping unsafe characters. */
  public SafeHtmlBuilder append(final CharSequence in) {
    if (in != null) {
      escapeCS(this, in);
    }
    return this;
  }

  /**
   * Open an element, appending "<tagName>" to the buffer.
   * <p>
   * After the element is open the attributes may be manipulated until the next
   * <code>append</code>, <code>openElement</code>, <code>closeSelf</code> or
   * <code>closeElement</code> call.
   *
   * @param tagName name of the HTML element to open.
   */
  public SafeHtmlBuilder openElement(final String tagName) {
    assert isElementName(tagName);
    cb.append("<");
    cb.append(tagName);
    if (sBuf == null) {
      att = new AttMap();
      sBuf = new BufferSealElement(this);
    }
    att.reset(tagName);
    cb = sBuf;
    return this;
  }

  /**
   * Get an attribute of the last opened element.
   *
   * @param name name of the attribute to read.
   * @return the attribute value, as a string. The empty string if the attribute
   *         has not been assigned a value. The returned string is the raw
   *         (unescaped) value.
   */
  public String getAttribute(final String name) {
    assert isAttributeName(name);
    assert cb == sBuf;
    return att.get(name);
  }

  /**
   * Set an attribute of the last opened element.
   *
   * @param name name of the attribute to set.
   * @param value value to assign; any existing value is replaced. The value is
   *        escaped (if necessary) during the assignment.
   */
  public SafeHtmlBuilder setAttribute(final String name, final String value) {
    assert isAttributeName(name);
    assert cb == sBuf;
    att.set(name, value != null ? value : "");
    return this;
  }

  /**
   * Set an attribute of the last opened element.
   *
   * @param name name of the attribute to set.
   * @param value value to assign, any existing value is replaced.
   */
  public SafeHtmlBuilder setAttribute(final String name, final int value) {
    return setAttribute(name, String.valueOf(value));
  }

  /**
   * Append a new value into a whitespace delimited attribute.
   * <p>
   * If the attribute is not yet assigned, this method sets the attribute. If
   * the attribute is already assigned, the new value is appended onto the end,
   * after appending a single space to delimit the values.
   *
   * @param name name of the attribute to append onto.
   * @param value additional value to append.
   */
  public SafeHtmlBuilder appendAttribute(final String name, String value) {
    if (value != null && value.length() > 0) {
      final String e = getAttribute(name);
      return setAttribute(name, e.length() > 0 ? e + " " + value : value);
    }
    return this;
  }

  /** Set the height attribute of the current element. */
  public SafeHtmlBuilder setHeight(final int height) {
    return setAttribute("height", height);
  }

  /** Set the width attribute of the current element. */
  public SafeHtmlBuilder setWidth(final int width) {
    return setAttribute("width", width);
  }

  /** Set the CSS class name for this element. */
  public SafeHtmlBuilder setStyleName(final String style) {
    assert isCssName(style);
    return setAttribute("class", style);
  }

  /**
   * Add an additional CSS class name to this element.
   *<p>
   * If no CSS class name has been specified yet, this method initializes it to
   * the single name.
   */
  public SafeHtmlBuilder addStyleName(final String style) {
    assert isCssName(style);
    return appendAttribute("class", style);
  }

  private void sealElement0() {
    assert cb == sBuf;
    cb = dBuf;
    att.onto(cb, this);
  }

  Buffer sealElement() {
    sealElement0();
    cb.append(">");
    return cb;
  }

  /** Close the current element with a self closing suffix ("/ &gt;"). */
  public SafeHtmlBuilder closeSelf() {
    sealElement0();
    cb.append(" />");
    return this;
  }

  /** Append a closing tag for the named element. */
  public SafeHtmlBuilder closeElement(final String name) {
    assert isElementName(name);
    cb.append("</");
    cb.append(name);
    cb.append(">");
    return this;
  }

  /** Append "&amp;nbsp;" - a non-breaking space, useful in empty table cells. */
  public SafeHtmlBuilder nbsp() {
    cb.append("&nbsp;");
    return this;
  }

  /** Append "&lt;br /&gt;" - a line break with no attributes */
  public SafeHtmlBuilder br() {
    cb.append("<br />");
    return this;
  }

  /** Append "&lt;tr&gt;"; attributes may be set if needed */
  public SafeHtmlBuilder openTr() {
    return openElement("tr");
  }

  /** Append "&lt;/tr&gt;" */
  public SafeHtmlBuilder closeTr() {
    return closeElement("tr");
  }

  /** Append "&lt;td&gt;"; attributes may be set if needed */
  public SafeHtmlBuilder openTd() {
    return openElement("td");
  }

  /** Append "&lt;/td&gt;" */
  public SafeHtmlBuilder closeTd() {
    return closeElement("td");
  }

  /** Append "&lt;div&gt;"; attributes may be set if needed */
  public SafeHtmlBuilder openDiv() {
    return openElement("div");
  }

  /** Append "&lt;/div&gt;" */
  public SafeHtmlBuilder closeDiv() {
    return closeElement("div");
  }

  /** Append "&lt;span&gt;"; attributes may be set if needed */
  public SafeHtmlBuilder openSpan() {
    return openElement("span");
  }

  /** Append "&lt;/span&gt;" */
  public SafeHtmlBuilder closeSpan() {
    return closeElement("span");
  }

  /** Append "&lt;a&gt;"; attributes may be set if needed */
  public SafeHtmlBuilder openAnchor() {
    return openElement("a");
  }

  /** Append "&lt;/a&gt;" */
  public SafeHtmlBuilder closeAnchor() {
    return closeElement("a");
  }

  /** Append "&lt;param name=... value=... /&gt;". */
  public SafeHtmlBuilder paramElement(final String name, final String value) {
    openElement("param");
    setAttribute("name", name);
    setAttribute("value", value);
    return closeSelf();
  }

  /** @return an immutable {@link SafeHtml} representation of the buffer. */
  public SafeHtml toSafeHtml() {
    return new SafeHtmlString(asString());
  }

  @Override
  public String asString() {
    return cb.toString();
  }

  private static void escapeCS(final SafeHtmlBuilder b, final CharSequence in) {
    for (int i = 0; i < in.length(); i++) {
      b.append(in.charAt(i));
    }
  }

  private static boolean isElementName(final String name) {
    return name.matches("^[a-zA-Z][a-zA-Z0-9_-]*$");
  }

  private static boolean isAttributeName(final String name) {
    return isElementName(name);
  }

  private static boolean isCssName(final String name) {
    return isElementName(name);
  }

  private static abstract class Impl {
    abstract void escapeStr(SafeHtmlBuilder b, String in);
  }

  private static class ServerImpl extends Impl {
    @Override
    void escapeStr(final SafeHtmlBuilder b, final String in) {
      SafeHtmlBuilder.escapeCS(b, in);
    }
  }

  private static class ClientImpl extends Impl {
    @Override
    void escapeStr(final SafeHtmlBuilder b, final String in) {
      b.cb.append(escape(in));
    }

    private static native String escape(String src)
    /*-{ return src.replace(/&/g,'&amp;')
                   .replace(/>/g,'&gt;')
                   .replace(/</g,'&lt;')
                   .replace(/"/g,'&quot;')
                   .replace(/'/g,'&#39;');
     }-*/;
  }
}