/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package com.bc.ceres.util; import java.io.FilterReader; import java.io.IOException; import java.io.Reader; import java.nio.CharBuffer; import java.util.Map; /** * A template reader replaces any occurences of <code>${<i>key</i>}</code> or <code>$<i>key</i></code> * in the underlying stream with the string representations of any non-null value returned by a given * resolver for that key. * * @author Norman Fomferra */ public class TemplateReader extends FilterReader { private static final char DEFAULT_KEY_INDICATOR = '$'; private static final int EOF = -1; private Resolver resolver; private IntBuffer buffer; private char keyIndicator = DEFAULT_KEY_INDICATOR; /** * Constructs a template reader for the given reader stream and a resolver given by a {@link java.util.Map}. * * @param in the underlying reader * @param map the map to serve as resolver */ public TemplateReader(Reader in, Map map) { this(in, new KeyValueResolver(map)); } /** * Constructs a template reader for the given reader stream and the given resolver. * * @param in the underlying reader * @param resolver the resolver */ public TemplateReader(Reader in, Resolver resolver) { super(in); if (resolver == null) { throw new NullPointerException("resolver"); } this.resolver = resolver; } /** * Gets the key indicator. * * @return the key indicator, defaults to '$'. */ public char getKeyIndicator() { return keyIndicator; } /** * Sets the key indicator. * * @param keyIndicator the key indicator, must not be a digit, letter or whitespace. */ public void setKeyIndicator(char keyIndicator) { if (Character.isWhitespace(keyIndicator) || Character.isLetterOrDigit(keyIndicator)) { throw new IllegalArgumentException(); } this.keyIndicator = keyIndicator; } /** * Reads all content. * @return the content * @throws java.io.IOException if an I/O error occurs */ public String readAll() throws IOException { StringBuilder sb = new StringBuilder(16 * 1024); while (true) { int i = read(); if (i == EOF) { break; } sb.append((char) i); } return sb.toString(); } /** * Read a single character. * * @throws java.io.IOException If an I/O error occurs */ @Override public int read() throws IOException { synchronized (lock) { if (buffer != null && buffer.ready()) { return buffer.next(); } int c = in.read(); if (c != keyIndicator) { return c; } if (buffer == null) { buffer = new IntBuffer(); } buffer.reset(); buffer.append(keyIndicator); c = readAndBuffer(); if (c != EOF) { int keyType = 0; if (c == '{') { // ${key}? do { c = readAndBuffer(); if (c == '}') { keyType = 1; break; } } while (c != EOF); } else if (Character.isJavaIdentifierStart(c)) { // $key? keyType = 2; do { c = readAndBuffer(); if (!(Character.isJavaIdentifierPart(c) || c == '.')) { break; } } while (c != EOF); } if (keyType != 0) { String key; if (keyType == 1) { // ${key} key = buffer.substring(2, buffer.length() - 1); } else { // $key key = buffer.substring(1, buffer.length() - 1); } Object value = resolver.resolve(key); if (value != null) { String s = value.toString(); int last; if (keyType == 1) { // ${key} last = in.read(); buffer.reset(); buffer.append(s); } else { // $key last = buffer.charAt(buffer.length() - 1); buffer.reset(); buffer.append(s); } buffer.append(last); // last can also be EOF! } } } return buffer.next(); } } /** * Read characters into an array. This method will block until some input * is available, an I/O error occurs, or the end of the stream is reached. * * @param cbuf Destination buffer * @return The number of characters read, or -1 * if the end of the stream * has been reached * @throws java.io.IOException If an I/O error occurs */ @Override public int read(char cbuf[]) throws IOException { return read(cbuf, 0, cbuf.length); } /** * Read characters into a portion of an array. * * @throws java.io.IOException If an I/O error occurs */ @Override public int read(char cbuf[], int off, int len) throws IOException { synchronized (lock) { int i; for (i = 0; i < len; i++) { int c = read(); if (c == EOF) { return i == 0 ? EOF : i; } cbuf[off + i] = (char) c; } return i; } } /** * Attempts to read characters into the specified character buffer. * The buffer is used as a repository of characters as-is: the only * changes made are the results of a put operation. No flipping or * rewinding of the buffer is performed. * * @param target the buffer to read characters into * @return The number of characters added to the buffer, or * -1 if this source of characters is at its end * @throws java.io.IOException if an I/O error occurs * @throws NullPointerException if target is null * @throws java.nio.ReadOnlyBufferException * if target is a read only buffer */ @Override public int read(CharBuffer target) throws IOException { synchronized (lock) { int len = target.remaining(); char[] cbuf = new char[len]; int n = read(cbuf, 0, len); if (n > 0) { target.put(cbuf, 0, n); } return n; } } /** * Skip characters. * * @throws java.io.IOException If an I/O error occurs */ @Override public long skip(long n) throws IOException { synchronized (lock) { if (n < 0L) { throw new IllegalArgumentException("skip value is negative"); } long i; for (i = 0; i < n; i++) { int c = read(); if (c == EOF) { break; } } return i; } } /** * Tell whether this stream is ready to be read. * * @throws java.io.IOException If an I/O error occurs */ @Override public boolean ready() throws IOException { return (buffer != null && buffer.ready()) || in.ready(); } /** * Tell whether this stream supports the mark() operation. */ @Override public boolean markSupported() { return false; } /** * Mark the present position in the stream. * * @throws java.io.IOException If an I/O error occurs */ @Override public void mark(int readAheadLimit) throws IOException { throw new IOException("mark() not supported"); } /** * Reset the stream. * * @throws java.io.IOException If an I/O error occurs */ @Override public void reset() throws IOException { throw new IOException("reset() not supported"); } /** * Close the stream. * * @throws java.io.IOException If an I/O error occurs */ @Override public void close() throws IOException { super.close(); buffer = null; } ///////////////////////////////////////////////////////////////////////////// private int readAndBuffer() throws IOException { int c = in.read(); buffer.append(c); return c; } ///////////////////////////////////////////////////////////////////////////// public static interface Resolver { Object resolve(String reference); } private static class KeyValueResolver implements Resolver { private Map map; public KeyValueResolver(Map map) { if (map == null) { throw new NullPointerException("map"); } this.map = map; } public Object resolve(String reference) { return map.get(reference); } } /** * A buffer to which EOF can also be appended. */ private static class IntBuffer { private final static int INC = 8192; private int[] buffer; private int length; private int index; public IntBuffer() { this.buffer = new int[INC]; } public void reset() { length = 0; index = 0; } public int next() { if (!ready()) { throw new IllegalStateException("!ready()"); } return buffer[index++]; } public int charAt(int pos) { if (pos < 0) { throw new IndexOutOfBoundsException("pos < 0"); } if (pos >= length) { throw new IndexOutOfBoundsException("pos >= length"); } return buffer[pos]; } public void append(int c) { if (length >= buffer.length) { int[] newBuffer = new int[buffer.length + INC]; System.arraycopy(buffer, 0, newBuffer, 0, length); buffer = newBuffer; } buffer[length++] = c; } public void append(String s) { for (int i = 0; i < s.length(); i++) { append((int) s.charAt(i)); } } public boolean ready() { return index < length; } public String substring(int start, int end) { int n = end - start; char[] cbuf = new char[n]; for (int i = start; i < end; i++) { cbuf[i - start] = (char) buffer[i]; } return new String(cbuf); } public int length() { return length; } } }