View Javadoc
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 }