001 /**
002 * Copyright 2005-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.krad.uif.freemarker;
017
018 import freemarker.core.Environment;
019 import freemarker.template.TemplateDirectiveBody;
020 import freemarker.template.TemplateDirectiveModel;
021 import freemarker.template.TemplateException;
022 import freemarker.template.TemplateModel;
023 import freemarker.template.TemplateModelException;
024
025 import java.io.IOException;
026 import java.io.Writer;
027 import java.util.Map;
028
029 /**
030 * A custom FreeMarker directive that adds escapes to nested content to make it valid for enclosure within a JSON
031 * string.
032 *
033 * <p>In other words, the content that is generated within this tag should be able to be enclosed in quotes within
034 * a JSON document without breaking strict JSON parsers. Note that this doesn't presently handle a wide variety of
035 * cases, just enough to properly escape basic html.</p>
036 *
037 * <p>
038 * There are three types of replacements this performs:
039 * <ul>
040 * <li>the quote character '"' is prefixed with a backslash</li>
041 * <li>newline characters are replaced with backslash followed by 'n'</li>
042 * <li>carriage return characters are replaced with backslash followed by 'r'</li>
043 * </ul>
044 * </p>
045 *
046 * @author Kuali Rice Team (rice.collab@kuali.org)
047 */
048 public class JsonStringEscapeDirective implements TemplateDirectiveModel {
049
050 @Override
051 public void execute(Environment env, Map params, TemplateModel[] loopVars,
052 TemplateDirectiveBody body) throws TemplateException, IOException {
053 // Check if no parameters were given:
054 if (!params.isEmpty()) {
055 throw new TemplateModelException(
056 getClass().getSimpleName() + " doesn't allow parameters.");
057 }
058 if (loopVars.length != 0) {
059 throw new TemplateModelException(
060 getClass().getSimpleName() + " doesn't allow loop variables.");
061 }
062
063 // If there is non-empty nested content:
064 if (body != null) {
065 // Executes the nested body. Same as <#nested> in FTL, except
066 // that we use our own writer instead of the current output writer.
067 body.render(new JsonEscapingFilterWriter(env.getOut()));
068 } else {
069 throw new RuntimeException("missing body");
070 }
071 }
072
073 /**
074 * A {@link Writer} that does escaping of nested content to make it valid for enclosure within a JSON string.
075 */
076 private static class JsonEscapingFilterWriter extends Writer {
077
078 private final Writer out;
079
080 /**
081 * Constructs a JsonEscapingFilterWriter which decorates the passed in Writer
082 *
083 * @param out the Writer to decorate
084 */
085 JsonEscapingFilterWriter(Writer out) {
086 this.out = out;
087 }
088
089 @Override
090 public void write(char[] cbuf, int off, int len) throws IOException {
091
092 // We need to allocate a buffer big enough to hold the escapes too, which take up extra chars
093 int needsEscapingCount = 0; // count up how many chars needing escapes are in the buffer
094
095 for (int i=0; i<len; i++) {
096 if (isNeedsEscaping(cbuf[i + off])) { needsEscapingCount += 1; }
097 }
098
099 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 }