BinaryStreamParser.java 10 KB
/*
 * This file is a part of EpnTAPClient.
 * This program aims to provide EPN-TAP support for software clients, like CASSIS spectrum analyzer.
 * See draft specifications: https://voparis-confluence.obspm.fr/pages/viewpage.action?pageId=559861
 * Copyright (C) 2016 Institut de Recherche en Astrophysique et Planétologie.
 *
 * 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 eu.omp.irap.vespa.votable.votabledata;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.bind.DatatypeConverter;

import eu.omp.irap.vespa.epntapclient.votable.model.DataType;
import eu.omp.irap.vespa.epntapclient.votable.model.Field;
import eu.omp.irap.vespa.epntapclient.votable.model.Stream;
import eu.omp.irap.vespa.votable.votable.VOTableException.CantParseVOTableException;

/**
 * @author N. Jourdane
 */
public final class BinaryStreamParser implements DataParser {

	/** The logger for the class BinaryStreamParser. */
	private static final Logger LOGGER = Logger.getLogger(BinaryStreamParser.class.getName());

	/** The index of the current position in the bytes array. */
	private int cursor;

	/** The XML nodes of VOTable document, containing the column names. */
	private List<Field> fieldsNodes;

	/** The byte array stored in the VOTable Table stream (if any). */
	private int rowSize;

	/** The stream to parse, containing data (column rows) stored in the VOTable. */
	private ByteBuffer stream;


	/**
	 * Constructor of BinaryStreamParser.
	 *
	 * @param voStream The stream to parse, containing data (column rows) stored in the VOTable.
	 * @param fieldsNodes the field XML nodes in VOTable document, containing the column names.
	 * @see <a href="http://www.ivoa.net/documents/REC/VOTable/VOTable-20040811.html#ToC35">The
	 *      Binary serialization in the VOTable REC.</a>
	 */
	public BinaryStreamParser(Stream voStream, List<Field> fieldsNodes) {
		cursor = 0;
		this.fieldsNodes = fieldsNodes;
		String strStream = voStream.getValue().replaceAll("(\\r|\\n)", "");
		stream = ByteBuffer.wrap(DatatypeConverter.parseBase64Binary(strStream));
	}

	/**
	 * Get the size of data block according to its type.
	 *
	 * @param dataType The type of the data block, as described in the VOTable REC. See
	 *            http://www.ivoa.net/documents/REC/VOTable/VOTable-20040811.html#ToC11
	 * @return The size of the block, in bytes.
	 */
	private static int getBlockSize(DataType dataType) {
		int blockSize;
		switch (dataType) {
		case BOOLEAN:
		case UNSIGNED_BYTE:
		case CHAR:
			blockSize = 1;
			break;
		case SHORT:
		case UNICODE_CHAR:
			blockSize = 2;
			break;
		case INT:
		case FLOAT:
			blockSize = 4;
			break;
		case LONG:
		case DOUBLE:
		case FLOAT_COMPLEX:
			blockSize = 8;
			break;
		case DOUBLE_COMPLEX:
			blockSize = 16;
			break;
		default:
			blockSize = 0;
			break;
		}
		return blockSize;
	}

	@Override
	public List<Object[]> parse() throws CantParseVOTableException {
		List<Object[]> data = new ArrayList<>();
		int nbColumns = fieldsNodes.size();
		BinaryStreamParser.LOGGER
				.info("Parsing data (" + nbColumns + " columns) in BINARY stream...");
		Object[] row = new Object[nbColumns];

		int nValue = 0;
		while (stream.hasRemaining()) {
			int colId = nValue % nbColumns;
			Field column = fieldsNodes.get(colId);

			rowSize = getRowSize(column);

			// May be useful: if arraySize == 0, continue

			if (colId == 0) {
				row = new Object[nbColumns];
			}

			row[colId] = processDataBlock(column.getDatatype());

			if (colId == nbColumns - 1) {
				data.add(row);
			}
			nValue++;
		}
		return data;
	}

	/**
	 * @param column The XML node of the VOTable data, containing informations about the column,
	 *            like the data primitive type.
	 * @return The size of the row, in bytes.
	 * @throws BufferUnderflowException Can not get the array size because there is no bytes to read
	 *             in the stream.
	 */
	private int getRowSize(Field column) {
		DataType dataType = column.getDatatype();
		int blockSize = getBlockSize(dataType);
		int arraySize;
		if (column.getArraysize() == null) {
			arraySize = blockSize;
		} else if ("*".equals(column.getArraysize())) {
			try {
				arraySize = stream.getInt() * blockSize;
			} catch (BufferUnderflowException e) {
				LOGGER.log(Level.SEVERE, "No space left on stream, can not get array size", e);
				throw new BufferUnderflowException();
			}
		} else {
			arraySize = Integer.parseInt(column.getArraysize()) * blockSize;
		}
		return arraySize;
	}

	/**
	 * @param dataType The type of the data block.
	 * @return An object representing the data. It can be any of the primitive type defined in the
	 *         VOtableREC. See http://www.ivoa.net/documents/REC/VOTable/VOTable-20040811.html#ToC11
	 */
	private Object processDataBlock(DataType dataType) {
		Object dataBlock;
		int blockSize = getBlockSize(dataType);
		if (dataType.equals(DataType.BOOLEAN)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(new Boolean(stream.getInt() == 0 ? false : true)));
					}
					dataBlock = sj.toString();
				}
			} else {
				dataBlock = new Boolean(stream.getInt() == 0 ? false : true);
			}
		} else if (dataType.equals(DataType.UNSIGNED_BYTE)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.get()));
					}
					dataBlock = sj.toString();
				}
			} else {
				dataBlock = stream.get();
			}
		} else if (dataType.equals(DataType.SHORT)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.getShort()));
					}
					dataBlock = sj.toString();
				}
			} else {
				dataBlock = stream.getShort();
			}
		} else if (dataType.equals(DataType.INT)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.getInt()));
					}
					dataBlock = sj.toString();
				}
			} else {
				dataBlock = stream.getInt();
			}
		} else if (dataType.equals(DataType.LONG)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.getLong()));
					}
					dataBlock = sj.toString();
				}
			} else {
				dataBlock = stream.getLong();
			}
		} else if (dataType.equals(DataType.CHAR)) {
			String value = new String();
			for (int i = 0; i < rowSize && cursor < stream.capacity()
					&& stream.position() < stream.limit(); i++) {
				value += (char) stream.get();
			}
			dataBlock = value.trim();
		} else if (dataType.equals(DataType.UNICODE_CHAR)) {
			String value = new String();
			for (int i = 0; i < rowSize && cursor < stream.capacity(); i += 2) {
				value += stream.getChar();
			}
			dataBlock = value.trim();
		} else if (dataType.equals(DataType.FLOAT)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.getFloat()));
					}
					dataBlock = sj.toString();
				}
			} else {
				dataBlock = stream.getFloat();
			}
		} else if (dataType.equals(DataType.DOUBLE)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.getDouble()));
					}
					dataBlock = sj.toString();
				}
			} else {
				dataBlock = stream.getDouble();
			}
		} else if (dataType.equals(DataType.FLOAT_COMPLEX)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.getFloat()));
						sj.add(String.valueOf(stream.getFloat()));
					}
					dataBlock = sj.toString();
				}
			} else {
				StringJoiner sj = new StringJoiner(" ", "", "");
				sj.add(String.valueOf(stream.getFloat()));
				sj.add(String.valueOf(stream.getFloat()));
				dataBlock = sj.toString();
			}			
		} else if (dataType.equals(DataType.DOUBLE_COMPLEX)) {
			if (rowSize != blockSize) {
				int nb = rowSize / blockSize;
				if (nb == 0) { dataBlock = ""; }
				else {
					StringJoiner sj = new StringJoiner(" ", "", "");
					for (int i = 0; i < nb; i++) {
						sj.add(String.valueOf(stream.getDouble()));
						sj.add(String.valueOf(stream.getDouble()));
					}
					dataBlock = sj.toString();
				}
			} else {
				StringJoiner sj = new StringJoiner(" ", "", "");
				sj.add(String.valueOf(stream.getDouble()));
				sj.add(String.valueOf(stream.getDouble()));
				dataBlock = sj.toString();
			}
		} else {
			BinaryStreamParser.LOGGER.warning("Data type " + dataType + " is not supported.");
			dataBlock = null;
		}
		return dataBlock;
	}
}