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 }