Generar archivo ZIP partiendo de múltiples ByteArray

Estos días he tenido que implementar una funcionalidad no excesivamente compleja, pero que implicaba lidiar con múltiples ByteArrayOutputStream. Como no encontré nada parecido a lo que me hacía falta, dejo aquí la solución por si a alguien le puede servir de ayuda.

El problema

Tenemos varios ByteArrayOutputStream (en mi caso venían devueltos por una herramienta de generación de informes) independientes, sobre los que queremos iterar (supongamos que llamamos a un servicio externo en cada iteración que nos devuelve el ByteArray) y construir un zip con un fichero que represente cada uno de esos ByteArray.

El planteamiento

Muchas de las soluciones que encontré partían de la base de crear un objeto File sobre un archivo ya existente, o crear un FileOutputStream que le pasaríamos al constructor del ByteArrayOutputStream, etc, etc. En cualquier caso, si se trataba de más de un fichero, iteraban sobre los FileInputstream que representaban el fichero y listo: para cada uno creabamos un ZipEntry que se añadía al ZipOutputStream, y hala, ZIP creado. Pero… ¿y si los ficheros sobre los que iteramos no son ficheros todavía? La cosa cambia. Y si hablamos de servlets, esto es más importante si cabe, ya que los permisos de lectura y escritura en el servidor no son cosa de risa.

Necesitabamos, pues, usar un stream en memoria, y cada uno de ellos guardarlo como una entrada ZIP en un ZipOutputStream, para luego devolver ese objeto al cliente (este último paso complicó más las cosas si cabe, ya que en un entorno que usa ZK no es tan simple recoger el objeto response. Del ServletResponse ya ni hablemos).

La solución

Necesitamos dos ByteArrayOutputStream. Si, dos. Uno para recoger cada uno de los objetos devueltos en la iteración que compone el ZIP, y el otro… pues una instancia inicialmente vacía que se pasa al constructor del ZipOutputStream. El código:

final ByteArrayOutputStream baosOut = new ByteArrayOutputStream();
final ZipOutputStream zos = new ZipOutputStream(baosOut);
try {
	for (final Map.Entry<String, ByteArrayOutputStream> m : mapBouts.entrySet()) {
		final ByteArrayOutputStream baosRpt = m.getValue();
		final ZipEntry entry = new ZipEntry(m.getKey());
		entry.setSize(baosRpt.toByteArray().length);
		zos.putNextEntry(entry);
		zos.write(baosRpt.toByteArray());
		zos.closeEntry();
	}
	if (zos != null) {
		zos.finish();
		zos.close();
		final Calendar cal = Calendar.getInstance();
		final DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
		sb = new StringBuilder(df.format(cal.getTime())).append("-").append(aName).append(".zip");
		// ZK
		Filedownload.save(baosOut.toByteArray(), "application/octet-stream", sb.toString());
	}
} catch (final IOException e) {
	// Exception Handler
} finally {
	if (zos != null) {
		try {
			zos.close();
		} catch (final IOException ex) {
			// Otro Exception Handler
		}
	}
}

Dejando de lado la parafernalia, el mapa mantiene una clave que será el nombre de cada uno de los archivos zip, y su valor es el array de bytes que representa el archivo en memoria. Para cada entrada del mapa, creamos el correspondiente archivo y lo añadimos al zip con ZipEntry. Una vez hemos recorrido el mapa, si el ZipOutputStream realmente existe, lo cerramos y le asignamos un nombre. Después lo enviamos al cliente, en este caso usando ZK.

La clave aquí es la segunda línea: al crear un ZipOutputStream y pasarle un ByteArrayOutputStream “vacío”, el primero irá “rellenando” este array de bytes a medida que vayamos incluyendo ZipEntry‘s en la iteración. Al finalizar, ese array de bytes contiene la representación en memoria del archivo zip, con lo cual solo queda enviarlo a un output para el cliente.

¿Y ahora que?

Hay una pequeña pega, que iré investigando cuando tenga tiempo. Las entradas del archivo zip, es decir, los ficheros que componen dicho archivo, no aceptan nombres en formato UTF-8. Cualquier carácter que exceda del 127 en la tabla ASCII se verá como un carácter extraño, así que tildes, eñes, cedillas y demás sufrirán este engorroso inconveniente. A los efectos no es un problema, ya que el zip se descomprime y los archivos abren perfectamente, pero visualmente queda horrible. Y si el archivo contiene muchos caracteres de este tipo… pues peor.

Mostrar y actualizar checkboxes en relaciones N:M con Spring e Hibernate

Supongamos una JOIN TABLE en un caso de relación N:M, como pueden ser profesores y materias: un profesor imparte una o varias materias, y una materia puede ser impartida por más de un profesor:

PROFESOR
id
nombre
apellidos

MATERIA
id
nombre
codigo

PROFESOR_MATERIA
profesor_id
materia_id

De aquí obtenemos dos clases, Profesor y Materia:

Profesor.java

@Entity
@Table(name = "profesor")
@PrimaryKeyJoinColumn(name = "persona_id", referencedColumnName = "id")
public class Profesor extends Person implements Serializable {

	@ManyToMany(fetch=FetchType.EAGER, cascade = {CascadeType.ALL})
	@JoinTable(name="profesor_materia",
	            joinColumns={@JoinColumn(name="profesor_id")},
	            inverseJoinColumns={@JoinColumn(name="materia_id")})
	private Set materias = new HashSet();

	public void setMaterias(Set materias) {
		this.materias = materias;
	}

	public Set getMaterias() {
		return materias;
	}
…

Materia.java

@Entity
@Table(name="materia")
public class Materia implements Serializable{

	@ManyToMany(fetch=FetchType.EAGER)
	@JoinTable(
			name="profesor_materia",
			joinColumns={@JoinColumn(name="materia_id", referencedColumnName="id")},
			inverseJoinColumns={@JoinColumn(name="profesor_id", referencedColumnName="id")}
		)
    	private Set profesores = new HashSet();

	// getters y setters
…

Suponiendo que queremos crear una vista donde podamos añadir profesores a una materia, la clave está en la anotación @JoinTable (incompatible con @MappedBy), donde especificamos mediante el atributo name que la tabla intermedia que queremos usar es PROFESOR_MATERIA; con joinColumns indicamos que la relación con MATERIA es mediante la columna materia_id, y que esta apunta al atributo id de la clase Materia.

inverseJoinColumns especifica la relación en sentido contrario, es decir, de PROFESOR a MATERIA. Indicamos, igual que antes, que la columna de la tabla intermedia es profesor_id, y que esta apunta al atributo id de la clase Profesor.

Completando ambas clases, y como parte fundamental del proceso de inserción y de actualización de registros, necesitamos sobrescribir los métodos equals y hashCode:

@Override
	public boolean equals(Object obj) {
	    if (this == obj)
	        return true;
	    if (obj == null)
	        return false;
	    if (!(obj instanceof Materia))
	        return false;
	    final Materia other = (Materia) obj;
	    if (getId() == null) {
	        if (other.getId() != null)
	            return false;
	    } else if (!getId().equals(other.getId()))
	        return false;
	    if (getNombre() == null) {
	        if (other.getNombre() != null)
	            return false;
	    } else if (!getNombre().equals(other.getNombre()))
	        return false;
	    return true;
	}

	@Override
	public int hashCode() {
	    final int prime = 31;
	    int result = 1;
	    result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
	    result = prime * result + ((getNombre() == null) ? 0 : getNombre().hashCode());
	    return result;
	}

Ahora pasamos al controlador. Lo primero que necesitaremos será un método que devuelva la lista de profesores disponibles para impartir nuestras materias, y asignarlo a un atributo de modelo:

@ModelAttribute("profesoresList")
	public ListpopulateWebProfesorList() {
		return this.profesorManager.getProfesores();
	}

Esto nos permitirá acceder en la vista a esta lista mediante el tag form:checkboxes, como veremos después. Pero antes, debemos implementar un método que le diga a Spring que debe hacer con cada uno de los elementos de tipo Profesor que le llegue cuando escojamos uno (o varios) profesores de nuestra lista. Para eso, usamos un bind:

@InitBinder
	public void initBinder(WebDataBinder binder) {
		binder.registerCustomEditor(Set.class, "profesores", new CustomCollectionEditor(Set.class){
	            protected Object convertElement(Object prof){
        		        if (prof instanceof String) {
                			Profesor profesor = profesorManager.getProfesor(prof.toString());
		                return profesor;
                		}
	                return null;
        		    }
	        });
	}

Para cada elemento de tipo Set de la propiedad “profesores” (siendo esta propiedad una colección de la clase Materias), realiza una conversión obteniendo la instancia apropiada de Profesor en función del id pasado como parámetro al ProfesorManager. Es decir, dado un id seleccionado en el checkbox, Spring obtiene la instancia correspondiente de Profesor.

Una vez hecho esto, ya podemos usar el tag en la vista:

<form:checkboxes path="profesores" items="${profesoresList}" itemLabel="nombre" itemValue="id" cssClass="checkboxes" />

Esto crea un conjunto de checks a partir de una lista (profesoresList) asociados a la propiedad correspondiente de la clase Materia (profesores), estableciendo como texto a mostrar el nombre (itemLabel), y el id como valor a enviar al servidor cada vez que se seleccione un elemento (itemValue).

Disable Foreign Keys in MySQL

Disable check of foreign keys, alter table, and enable them again:

SET foreign_key_checks = 0;
alter table `table_name` drop foreign key `foreign_key_name`;
ALTER TABLE `table_name` DROP `column_name`;
SET foreign_key_checks = 1;

Dropdowns with Enums in Grails

GSP:

<g:select name="personType"
      from="${es.xoubin.labgrails.PersonType?.values()}"
      keys="${es.xoubin.labgrails.PersonType.values()*.code()}"
      value="${personInstance?.personType}"
      valueMessagePrefix="person.enum.personType" />

Domain:

 enum PersonType {
 EMPLOYEE('E'), USER('U'), MANAGER('M'), PUBLIC('P')
 private final String code
 PersonType(String code) {
 this.code = code
 }
 public String code() {
 return code
 }
 }
 class Person {
 [...]
 String personType
 [...]
 }

messages:

person.enum.personType.E=Employee!!
person.enum.personType.U=An user
person.enum.personType.M=Eficient manager
person.enum.personType.P=Anonymous public