1 /**
2 * Copyright 2005-2014 The Kuali Foundation
3 *
4 * Licensed under the Educational Community License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.opensource.org/licenses/ecl2.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.kuali.rice.krad.uif.freemarker;
17
18 import freemarker.core.Environment;
19 import freemarker.template.TemplateDirectiveBody;
20 import freemarker.template.TemplateDirectiveModel;
21 import freemarker.template.TemplateException;
22 import freemarker.template.TemplateModel;
23 import freemarker.template.TemplateModelException;
24
25 import java.io.IOException;
26 import java.io.Writer;
27 import java.util.Map;
28
29 /**
30 * A custom FreeMarker directive that adds escapes to nested content to make it valid for enclosure within a JSON
31 * string.
32 *
33 * <p>In other words, the content that is generated within this tag should be able to be enclosed in quotes within
34 * a JSON document without breaking strict JSON parsers. Note that this doesn't presently handle a wide variety of
35 * cases, just enough to properly escape basic html.</p>
36 *
37 * <p>
38 * There are three types of replacements this performs:
39 * <ul>
40 * <li>the quote character '"' is prefixed with a backslash</li>
41 * <li>newline characters are replaced with backslash followed by 'n'</li>
42 * <li>carriage return characters are replaced with backslash followed by 'r'</li>
43 * </ul>
44 * </p>
45 *
46 * @author Kuali Rice Team (rice.collab@kuali.org)
47 */
48 public class JsonStringEscapeDirective implements TemplateDirectiveModel {
49
50 @Override
51 public void execute(Environment env, Map params, TemplateModel[] loopVars,
52 TemplateDirectiveBody body) throws TemplateException, IOException {
53 // Check if no parameters were given:
54 if (!params.isEmpty()) {
55 throw new TemplateModelException(
56 getClass().getSimpleName() + " doesn't allow parameters.");
57 }
58 if (loopVars.length != 0) {
59 throw new TemplateModelException(
60 getClass().getSimpleName() + " doesn't allow loop variables.");
61 }
62
63 // If there is non-empty nested content:
64 if (body != null) {
65 // Executes the nested body. Same as <#nested> in FTL, except
66 // that we use our own writer instead of the current output writer.
67 body.render(new JsonEscapingFilterWriter(env.getOut()));
68 } else {
69 throw new RuntimeException("missing body");
70 }
71 }
72
73 /**
74 * A {@link Writer} that does escaping of nested content to make it valid for enclosure within a JSON string.
75 */
76 private static class JsonEscapingFilterWriter extends Writer {
77
78 private final Writer out;
79
80 /**
81 * Constructs a JsonEscapingFilterWriter which decorates the passed in Writer
82 *
83 * @param out the Writer to decorate
84 */
85 JsonEscapingFilterWriter(Writer out) {
86 this.out = out;
87 }
88
89 @Override
90 public void write(char[] cbuf, int off, int len) throws IOException {
91
92 // We need to allocate a buffer big enough to hold the escapes too, which take up extra chars
93 int needsEscapingCount = 0; // count up how many chars needing escapes are in the buffer
94
95 for (int i=0; i<len; i++) {
96 if (isNeedsEscaping(cbuf[i + off])) { needsEscapingCount += 1; }
97 }
98
99 char[] transformedCbuf = new char[len + needsEscapingCount]; // allocate additional space for escapes
100 int escapesAddedCount = 0; // the count of how many chars we've had to escape so far
101
102 for (int i = 0; i < len; i++) {
103 if (isNeedsEscaping(cbuf[i + off])) {
104 transformedCbuf[i+escapesAddedCount] = '\\';
105 escapesAddedCount += 1;
106 }
107
108 if (cbuf[i + off] == '\n') {
109 // newlines need to be replaced with literal "\n" <-- two chars
110 transformedCbuf[i+escapesAddedCount] = 'n';
111 } else if (cbuf[i + off] == '\r') {
112 // carriage returns need to be replaced with literal "\r" <-- two chars
113 transformedCbuf[i+escapesAddedCount] = 'r';
114 } else {
115 // standard escaping where we still use the original char
116 transformedCbuf[i+escapesAddedCount] = cbuf[i + off];
117 }
118 }
119
120 out.write(transformedCbuf);
121 }
122
123 @Override
124 public void flush() throws IOException {
125 out.flush();
126 }
127
128 @Override
129 public void close() throws IOException {
130 out.close();
131 }
132
133 /**
134 * Does the given character need escaping to be rendered as part of a JSON string?
135 *
136 * @param c the character to test
137 * @return true if the character needs escaping for inclusion in a JSON string.
138 */
139 private static boolean isNeedsEscaping(char c) {
140 return (c == '"' || c == '\n' || c == '\r');
141 }
142 }
143 }