001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.template.utility;
054:
055: import java.io.IOException;
056: import java.io.Writer;
057: import java.util.Map;
058:
059: import freemarker.template.TemplateBooleanModel;
060: import freemarker.template.TemplateModelException;
061: import freemarker.template.TemplateNumberModel;
062: import freemarker.template.TemplateTransformModel;
063:
064: /**
065: * <p>A filter that compresses each sequence of consecutive whitespace
066: * to a single line break (if the sequence contains a line break) or a
067: * single space. In addition, leading and trailing whitespace is
068: * completely removed.</p>
069: *
070: * <p>Specify the transform parameter <code>single_line = true</code>
071: * to always compress to a single space instead of a line break.</p>
072: *
073: * <p>The default buffer size can be overridden by specifying a
074: * <code>buffer_size</code> transform parameter (in bytes).</p>
075: *
076: * <p><b>Note:</b> The compress tag is implemented using this filter</p>
077: *
078: * <p>Usage:<br />
079: * From java:</p>
080: * <pre>
081: * SimpleHash root = new SimpleHash();
082: *
083: * root.put( "standardCompress", new freemarker.template.utility.StandardCompress() );
084: *
085: * ...
086: * </pre>
087: *
088: * <p>From your FreeMarker template:</p>
089: * <pre>
090: * <transform standardCompress>
091: * <p>This paragraph will have
092: * extraneous
093: *
094: * whitespace removed.</p>
095: * </transform>
096: * </pre>
097: *
098: * <p>Output:</p>
099: * <pre>
100: * <p>This paragraph will have
101: * extraneous
102: * whitespace removed.</p>
103: * </pre>
104: *
105: * @version $Id: StandardCompress.java,v 1.14 2004/01/06 17:06:43 szegedia Exp $
106: */
107: public class StandardCompress implements TemplateTransformModel {
108: private static final String BUFFER_SIZE_KEY = "buffer_size";
109: private static final String SINGLE_LINE_KEY = "single_line";
110: private int defaultBufferSize;
111:
112: public static final StandardCompress INSTANCE = new StandardCompress();
113:
114: public StandardCompress() {
115: this (2048);
116: }
117:
118: /**
119: * @param defaultBufferSize the default amount of characters to buffer
120: */
121: public StandardCompress(int defaultBufferSize) {
122: this .defaultBufferSize = defaultBufferSize;
123: }
124:
125: public Writer getWriter(final Writer out, Map args)
126: throws TemplateModelException {
127: int bufferSize = defaultBufferSize;
128: boolean singleLine = false;
129: if (args != null) {
130: try {
131: TemplateNumberModel num = (TemplateNumberModel) args
132: .get(BUFFER_SIZE_KEY);
133: if (num != null)
134: bufferSize = num.getAsNumber().intValue();
135: } catch (ClassCastException e) {
136: throw new TemplateModelException(
137: "Expecting numerical argument to "
138: + BUFFER_SIZE_KEY);
139: }
140: try {
141: TemplateBooleanModel flag = (TemplateBooleanModel) args
142: .get(SINGLE_LINE_KEY);
143: if (flag != null)
144: singleLine = flag.getAsBoolean();
145: } catch (ClassCastException e) {
146: throw new TemplateModelException(
147: "Expecting boolean argument to "
148: + SINGLE_LINE_KEY);
149: }
150: }
151: return new StandardCompressWriter(out, bufferSize, singleLine);
152: }
153:
154: private static class StandardCompressWriter extends Writer {
155: private static final int MAX_EOL_LENGTH = 2; // CRLF is two bytes
156:
157: private static final int AT_BEGINNING = 0;
158: private static final int SINGLE_LINE = 1;
159: private static final int INIT = 2;
160: private static final int SAW_CR = 3;
161: private static final int LINEBREAK_CR = 4;
162: private static final int LINEBREAK_CRLF = 5;
163: private static final int LINEBREAK_LF = 6;
164:
165: private final Writer out;
166: private final char[] buf;
167: private final boolean singleLine;
168:
169: private int pos = 0;
170: private boolean inWhitespace = true;
171: private int lineBreakState = AT_BEGINNING;
172:
173: public StandardCompressWriter(Writer out, int bufSize,
174: boolean singleLine) {
175: this .out = out;
176: this .singleLine = singleLine;
177: buf = new char[bufSize];
178: }
179:
180: public void write(char[] cbuf, int off, int len)
181: throws IOException {
182: for (;;) {
183: // Need to reserve space for the EOL potentially left in the state machine
184: int room = buf.length - pos - MAX_EOL_LENGTH;
185: if (room >= len) {
186: writeHelper(cbuf, off, len);
187: break;
188: } else if (room <= 0) {
189: flushInternal();
190: } else {
191: writeHelper(cbuf, off, room);
192: flushInternal();
193: off += room;
194: len -= room;
195: }
196: }
197: }
198:
199: private void writeHelper(char[] cbuf, int off, int len) {
200: for (int i = off, end = off + len; i < end; i++) {
201: char c = cbuf[i];
202: if (Character.isWhitespace(c)) {
203: inWhitespace = true;
204: updateLineBreakState(c);
205: } else if (inWhitespace) {
206: inWhitespace = false;
207: writeLineBreakOrSpace();
208: buf[pos++] = c;
209: } else {
210: buf[pos++] = c;
211: }
212: }
213: }
214:
215: /*
216: \r\n => CRLF
217: \r[^\n] => CR
218: \r$ => CR
219: [^\r]\n => LF
220: ^\n => LF
221: */
222: private void updateLineBreakState(char c) {
223: switch (lineBreakState) {
224: case INIT:
225: if (c == '\r') {
226: lineBreakState = SAW_CR;
227: } else if (c == '\n') {
228: lineBreakState = LINEBREAK_LF;
229: }
230: break;
231: case SAW_CR:
232: if (c == '\n') {
233: lineBreakState = LINEBREAK_CRLF;
234: } else {
235: lineBreakState = LINEBREAK_CR;
236: }
237: }
238: }
239:
240: private void writeLineBreakOrSpace() {
241: switch (lineBreakState) {
242: case SAW_CR:
243: // whitespace ended with CR, fall through
244: case LINEBREAK_CR:
245: buf[pos++] = '\r';
246: break;
247: case LINEBREAK_CRLF:
248: buf[pos++] = '\r';
249: // fall through
250: case LINEBREAK_LF:
251: buf[pos++] = '\n';
252: break;
253: case AT_BEGINNING:
254: // ignore leading whitespace
255: break;
256: case INIT:
257: case SINGLE_LINE:
258: buf[pos++] = ' ';
259: }
260: lineBreakState = (singleLine) ? SINGLE_LINE : INIT;
261: }
262:
263: private void flushInternal() throws IOException {
264: out.write(buf, 0, pos);
265: pos = 0;
266: }
267:
268: public void flush() throws IOException {
269: flushInternal();
270: out.flush();
271: }
272:
273: public void close() throws IOException {
274: flushInternal();
275: }
276: }
277: }
|