Java: Universal Placeholder for InputStreams

Los ficheros XML tienen una sintaxis muy completa y los ficheros de Properties un formato sencillo de manipular:

# placeholders.properties

user = root
password = secret

Y como queremos ambas cosas, los “placeholders” nos permiten hacer el mix de lo mejor de un lenguaje de marcado como XML con la sencillez de un fichero de propiedades, ejemplo:

# config.xml

<config>
  <init-param>
    <param-name>user</param-name>
    <param-value>${user}</param-value>
  </init-param>
  <init-param>
    <param-name>password</param-name>
    <param-value>${password}</param-value>
  </init-param>
</config>

La idea detras de esto es incluir el valor de “user” y “password” (extraidos del fichero de propiedades) dentro del XML. Y aqui una implementación muy sencilla y generica que permite usar placeholders en cualquier InputStream, independiente del formato:

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

public class PlaceholderInputStream extends InputStream {
	public static final int BUFFER_LENGTH = 4096;
	//
	final PushbackInputStream is;
	final PlaceholderMapper mapper;
	//
	final StringBuilder unreadBuffer = new StringBuilder();
	final StringBuilder placeHolderBuffer = new StringBuilder();
	//
	State state = State.WANT_DOLAR_SIGN;

	public PlaceholderInputStream(final InputStream is, 
			final PlaceholderMapper mapper) throws IOException {
		this.is = new PushbackInputStream(is, BUFFER_LENGTH);
		this.mapper = mapper;
	}

	protected void processByte(final int b) throws IOException {
		switch (state) {
			case WANT_DOLAR_SIGN:
				if (b == '$') {
					state = State.WANT_BRACE_BEGIN;
				} else {
					unreadBuffer.append((char) b);
				}
				break;
			case WANT_BRACE_BEGIN:
				if (b == '{') {
					state = State.WANT_BRACE_END;
				} else {
					state = State.WANT_BRACE_BEGIN;
					unreadBuffer.append((char) b);
				}
				break;
			case WANT_BRACE_END:
				if (b == '}') {
					state = State.WANT_DOLAR_SIGN;
					final String propName = placeHolderBuffer.toString();
					String value = mapper.mapPlaceHolder(propName);
					if (value == null)
						value = "${" + propName + "}";
					unreadBuffer.append(value);
					placeHolderBuffer.setLength(0);
				} else {
					placeHolderBuffer.append((char) b);
				}
				break;
		}
	}

	@Override
	public void close() throws IOException {
		is.close();
	}

	@Override
	public int available() throws IOException {
		return is.available();
	}

	@Override
	public int read() throws IOException {
		processByte(is.read());
		if (unreadBuffer.length() > 0) {
			is.unread(unreadBuffer.toString().getBytes());
			unreadBuffer.setLength(0);
		}
		return is.read();
	}

	@Override
	public int read(final byte[] b) throws IOException {
		if (b != null) {
			final byte[] buf = new byte[b.length];
			int read = is.read(buf);
			for (int i = 0; i < read; i++) {
				processByte(buf[i]);
			}
			if (unreadBuffer.length() > 0) {
				is.unread(unreadBuffer.toString().getBytes());
				unreadBuffer.setLength(0);
			}
		}
		return is.read(b);
	}

	@Override
	public int read(final byte[] b, final int off, final int len) 
			throws IOException {
		if (b != null) {
			final byte[] buf = new byte[len];
			int read = is.read(buf);
			for (int i = 0; i < read; i++) {
				processByte(buf[i]);
			}
			if (unreadBuffer.length() > 0) {
				is.unread(unreadBuffer.toString().getBytes());
				unreadBuffer.setLength(0);
			}
		}
		return is.read(b, off, len);
	}

	@Override
	public long skip(final long n) throws IOException {
		return is.skip(n);
	}

	@Override
	public boolean markSupported() {
		return is.markSupported();
	}

	@Override
	public synchronized void mark(final int readlimit) {
		is.mark(readlimit);
	}

	@Override
	public synchronized void reset() throws IOException {
		is.reset();
	}

	private static enum State {
		WANT_DOLAR_SIGN, WANT_BRACE_BEGIN, WANT_BRACE_END;
	}

	/**
	 * Interface for callback used for mapping placeholders
	 */
	public static interface PlaceholderMapper {
		public String mapPlaceHolder(final String name);
	}
}

Un ejemplo de uso plug&play:

public static void main(final String[] args) throws Throwable {
	final Properties base = new Properties();
	final PlaceholderMapper mapper = new PlaceholderMapper() {
		@Override
		public String mapPlaceHolder(final String name) {
			return base.getProperty(name);
		}
	};
	InputStream prop = null, xml = null, phis = null;
	try {
		prop = mapper.getClass().getResourceAsStream("/placeholders.properties");
		xml = mapper.getClass().getResourceAsStream("/config.xml");
		phis = new PlaceholderInputStream(xml, mapper);
		base.load(prop);
		final byte[] bb = new byte[4096];
		final int read = phis.read(bb);
		System.out.println(new String(bb, 0, read));
	} finally {
		if (phis != null)
			phis.close();
		if (xml != null)
			xml.close();
		if (prop != null)
			prop.close();
	}
}

Y aquí el resultado:

<config>
  <init-param>
    <param-name>user</param-name>
    <param-value>root</param-value>
  </init-param>
  <init-param>
    <param-name>password</param-name>
    <param-value>secret</param-value>
  </init-param>
</config>

Con unos mínimos cambios se puede hacer que el PlaceholderMapper use SystemProperties o cualquier otra fuente de datos en lugar de un fichero.

Source code: PlaceholderPushbackInputStream.java

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: