/* * Copyright 2015 Daniel Dittmar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package dan.dit.whatsthat.util.compaction; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * A Compacter is a very simple object that allows 'serialization' of * primitive data types by compacting data into a single string as CSV (see http://en.wikipedia.org/wiki/Comma-separated_values). * This data * that is appended to the Compacter can later be read by a new Compacter initialized * with the compacted string in the same order. Strings must not be null, but can contain any data! So there is no special character excluded. * <br> Nesting of * Compacters is easily possible, so strings compacted by a Compacter can be appended and later * read by another Compacter. Though this is not encouraged since output data will grow in length exponentially for every nesting depth.<br><br> * Example:<br> * <code>Compacter cmp = new Compacter().appendData("a").appendData("b").appendData(2);<br> * String compacted = cmp.compact();<br> * Compacter decompacter = new Compacter(compacted);<br> * // decompacter.getData(0) equals "a", getData(1) equals "b" and getData(2) equals String.valueOf(2) </code> * @author Daniel * */ public class Compacter implements Iterable<String> { private static final String SEPARATION_SYMBOL = ";"; // any length one string which is no blank symbol that gets trimmed private static final String BLANK = " "; // any string of length >= 1 unequal to and does not contain SEPARATION_SYMBOL private static final String SEPARATOR = BLANK + SEPARATION_SYMBOL + BLANK; private static final char[] SEPERATOR_CHARS = SEPARATOR.toCharArray(); private List<String> data; /** * Creates a new (De)Compacter which decompacts the given compacted data. * @param compactedData Compacted data that can then be read. * @throws IllegalArgumentException If given data string is <code>null</code>. */ public Compacter(String compactedData) { if (compactedData == null) { throw new IllegalArgumentException("Given compacted data is null, consider using an empty String for empty data."); } data = new ArrayList<>(); // search for SEPERATORS and extract data in between int startIndex = 0; final int dataLength = compactedData.length(); final int seperatorLength = SEPERATOR_CHARS.length; for (int curIndex = 0; curIndex < dataLength - seperatorLength + 1; ) { boolean foundSeparator = true; for (int s = 0; s < seperatorLength; s++) { if (SEPERATOR_CHARS[s] != compactedData.charAt(curIndex + s)) { foundSeparator = false; break; } } if (foundSeparator) { data.add(compactedData.substring(startIndex, curIndex).replace(SEPARATION_SYMBOL + SEPARATION_SYMBOL, SEPARATION_SYMBOL)); curIndex += seperatorLength; startIndex = curIndex; } else { curIndex++; } } if (startIndex < dataLength) { data.add(compactedData.substring(startIndex).replace(SEPARATION_SYMBOL + SEPARATION_SYMBOL, SEPARATION_SYMBOL)); } } /** * Creates a new empty Compacter with default capacity. */ public Compacter() { data = new ArrayList<>(); } /** * Creates a new empty Compacter with the given capacity. * @param capacity The capacity for the Compacter, must be positive. */ public Compacter(int capacity) { data = new ArrayList<>(capacity); } /** * Appends data to the Compacter. * @param dataString The data string to append. * @throws IllegalArgumentException If dataString is <code>null</code>. * @return this */ public Compacter appendData(String dataString) { if (dataString == null) { throw new IllegalArgumentException("Given String is null."); } data.add(dataString); return this; } /** * Appends the given int. Equal to appendData(String.valueOf(dataInt)). * @param dataInt The int to append. * @return this */ public Compacter appendData(int dataInt) { data.add(String.valueOf(dataInt)); return this; } /** * Appends the given char. Equal to appendData(String.valueOf(dataChar)). * @param dataChar The char to append. * @return this */ public Compacter appendData(char dataChar) { data.add(String.valueOf(dataChar)); return this; } /** * Appends the given long. Equal to appendData(String.valueOf(dataLong)). * @param dataLong The long to append. * @return this */ public Compacter appendData(long dataLong) { data.add(String.valueOf(dataLong)); return this; } public Compacter appendData(double data) { long ldata = Double.doubleToLongBits(data); appendData(ldata); return this; } public Compacter appendData(float data) { int idata = Float.floatToIntBits(data); appendData(idata); return this; } public float getFloat(int index) throws CompactedDataCorruptException { int idata = getInt(index); return Float.intBitsToFloat(idata); } public double getDouble(int index) throws CompactedDataCorruptException { long ldata = getLong(index); return Double.longBitsToDouble(ldata); } public Compacter appendData(boolean data) { if (data) { appendData("1"); } else { appendData("0"); } return this; } public boolean getBoolean(int index) { return data.get(index).equals("1"); } /** * Returns the data at the given index. The data order is kept consistent: * The first data appended is at index 0, the second at index 1,... * @param index The index of the desired data. Must be greater than or equal zero * and lower than getSize(). * @return The data stored at the given index. */ public String getData(int index) { return data.get(index); } /** * Returns the data at the given index, converting it to an integer. * @param index The index of the desired data. Must be greater than or equal zero and lower than getSize(). * @return The data stored ath the given index as an integer. * @throws CompactedDataCorruptException When a NumberFormatException occurs. */ public int getInt(int index) throws CompactedDataCorruptException { String dataString = data.get(index); int dataInt; try { dataInt = Integer.parseInt(dataString); } catch (NumberFormatException nfe) { throw new CompactedDataCorruptException("Could not parse int from: " + dataString).setCorruptData(this); } return dataInt; } /** * Returns the data at the given index, converting it to a long. * @param index The index of the desired data. Must be greater than or equal zero and lower than getSize(). * @return The data stored ath the given index as a long. * @throws CompactedDataCorruptException When a NumberFormatException occurs. */ public long getLong(int index) throws CompactedDataCorruptException { String dataString = data.get(index); long dataLong; try { dataLong = Long.parseLong(dataString); } catch (NumberFormatException nfe) { throw new CompactedDataCorruptException("Could not parse long from: " + dataString).setCorruptData(this); } return dataLong; } @Override public Iterator<String> iterator() { return data.iterator(); } /** * Returns the size of this Compacter, which is the amount * of data packages stored with it. * @return The size of this Compacter. */ public int getSize() { return data.size(); } public String compact() { // compacting is always possible if (data.size() == 0) { return ""; } StringBuilder builder = new StringBuilder(); for (int i = 0; i < data.size(); i++) { String s = data.get(i); builder.append(s.replace(SEPARATION_SYMBOL, SEPARATION_SYMBOL + SEPARATION_SYMBOL)); if (i < data.size() - 1 || s.length() == 0) { builder.append(SEPARATOR); } } return builder.toString(); } @Override public String toString() { return compact(); } @Override public int hashCode() { return data.hashCode(); } @Override public boolean equals(Object other) { if (other instanceof Compacter) { return data.equals(((Compacter) other).data); } else { return super.equals(other); } } }