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    }