/* * Copyright (c) 2008, 2009, 2011, 2012, 2015 Eike Stepper (Berlin, Germany) and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Eike Stepper - initial API and implementation */ package org.eclipse.net4j.util.io; import org.eclipse.net4j.util.CheckUtil; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Eike Stepper * @since 2.0 */ public class StringCompressor implements StringIO { /** * @since 3.0 */ public static boolean BYPASS = false; private static final int NULL_ID = 0; private static final int INFO_FOLLOWS = Integer.MIN_VALUE; private static final byte NOTHING_FOLLOWS = 1; private static final byte STRING_FOLLOWS = 2; private static final byte ACK_FOLLOWS = 3; private static final boolean DEBUG = false; private static final byte DEBUG_STRING = -1; private static final byte DEBUG_INT = -2; private static final byte DEBUG_BYTE = -3; private boolean client; private int lastID; private Map<String, ID> stringToID = new HashMap<String, ID>(); private Map<Integer, String> idToString = new HashMap<Integer, String>(); private List<Integer> pendingAcknowledgements = new ArrayList<Integer>(); /** * Creates a StringCompressor instance. * * @param client * Must be different on both sides of the stream. */ public StringCompressor(boolean client) { this.client = client; } public boolean isClient() { return client; } public void write(ExtendedDataOutput out, String string) throws IOException { if (DEBUG) { trace("BEGIN", string); } if (string == null) { writeInt(out, NULL_ID); return; } ID id; List<Integer> acknowledgements = null; boolean stringFollows = false; synchronized (this) { id = stringToID.get(string); if (id == null) { lastID += client ? 1 : -1; id = new ID(lastID); stringToID.put(string, id); idToString.put(id.getValue(), string); stringFollows = true; } else if (!id.isAcknowledged()) { stringFollows = true; } if (!pendingAcknowledgements.isEmpty()) { acknowledgements = pendingAcknowledgements; pendingAcknowledgements = new ArrayList<Integer>(); } } if (stringFollows || acknowledgements != null) { writeInt(out, INFO_FOLLOWS); writeInt(out, id.getValue()); if (stringFollows) { writeByte(out, STRING_FOLLOWS); writeString(out, string); } if (acknowledgements != null) { for (int ack : acknowledgements) { writeByte(out, ACK_FOLLOWS); writeInt(out, ack); } } writeByte(out, NOTHING_FOLLOWS); } else { writeInt(out, id.getValue()); } } public String read(ExtendedDataInput in) throws IOException { if (DEBUG) { trace("BEGIN", "?"); } int id = readInt(in); if (id == NULL_ID) { return null; } String string = null; List<Integer> acks = null; if (id == INFO_FOLLOWS) { id = readInt(in); boolean moreInfos = true; while (moreInfos) { byte info = readByte(in); switch (info) { case NOTHING_FOLLOWS: moreInfos = false; break; case STRING_FOLLOWS: string = readString(in); break; case ACK_FOLLOWS: if (acks == null) { acks = new ArrayList<Integer>(); } acks.add(readInt(in)); break; default: throw new IOException("Invalid info: " + info); //$NON-NLS-1$ } } } synchronized (this) { acknowledge(acks); if (string != null) { stringToID.put(string, new ID(id)); idToString.put(id, string); pendingAcknowledgements.add(id); } else { string = idToString.get(id); if (string == null) { throw new IOException("String ID unknown: " + id); //$NON-NLS-1$ } } } return string; } @Override public String toString() { return MessageFormat.format("StringCompressor[client={0}]", client); //$NON-NLS-1$ } private void acknowledge(List<Integer> acks) { if (acks != null) { for (int value : acks) { String string = idToString.get(value); if (string != null) { ID id = stringToID.get(string); if (id != null) { id.setAcknowledged(); } } } } } private void writeByte(ExtendedDataOutput out, byte value) throws IOException { if (DEBUG) { trace("writeByte", value); out.writeByte(DEBUG_BYTE); } out.writeByte(value); } private void writeInt(ExtendedDataOutput out, int value) throws IOException { if (DEBUG) { trace("writeInt", value); out.writeByte(DEBUG_INT); } out.writeInt(value); } /** * @since 3.0 */ protected void writeString(ExtendedDataOutput out, String value) throws IOException { if (DEBUG) { trace("writeString", value); out.writeByte(DEBUG_STRING); } out.writeString(value); } private byte readByte(ExtendedDataInput in) throws IOException { if (DEBUG) { byte type = in.readByte(); if (DEBUG_BYTE != type) { throw new IOException("Not a byte value (type=" + type + ")"); //$NON-NLS-1$ } } byte value = in.readByte(); if (DEBUG) { trace("readByte", value); } return value; } private int readInt(ExtendedDataInput in) throws IOException { if (DEBUG) { byte type = in.readByte(); if (DEBUG_INT != type) { throw new IOException("Not an integer value (type=" + type + ")"); //$NON-NLS-1$ } } int value = in.readInt(); if (DEBUG) { trace("readInt", value); } return value; } /** * @since 3.0 */ protected String readString(ExtendedDataInput in) throws IOException { if (DEBUG) { byte type = in.readByte(); if (DEBUG_STRING != type) { throw new IOException("Not a string value (type=" + type + ")"); //$NON-NLS-1$ } } String value = in.readString(); if (DEBUG) { trace("readString", value); } return value; } private void trace(String prefix, Object value) { if (value instanceof Byte) { byte opcode = (Byte)value; switch (opcode) { case NOTHING_FOLLOWS: value = "NOTHING_FOLLOWS"; break; case STRING_FOLLOWS: value = "STRING_FOLLOWS"; break; case ACK_FOLLOWS: value = "STRING_FOLLOWS"; break; } } if (value instanceof Integer) { int opcode = (Integer)value; if (opcode == INFO_FOLLOWS) { value = "INFO_FOLLOWS"; } } String msg = "[" + Thread.currentThread().getName() + "] " + prefix + ": " + value; if (!client) { msg = " " + msg; } IOUtil.OUT().println(msg); } /** * @author Eike Stepper */ private static final class ID { private int value; private boolean acknowledged; public ID(int value) { CheckUtil.checkArg(value != INFO_FOLLOWS, "value"); this.value = value; } public int getValue() { return value; } public boolean isAcknowledged() { return acknowledged; } public void setAcknowledged() { acknowledged = true; } } /** * @author Eike Stepper * @since 3.0 */ public static class Counting extends StringCompressor { private long stringsRead; private long stringsWritten; public Counting(boolean client) { super(client); } public long getStringsRead() { return stringsRead; } public long getStringsWritten() { return stringsWritten; } @Override protected String readString(ExtendedDataInput in) throws IOException { synchronized (this) { ++stringsRead; } return super.readString(in); } @Override protected void writeString(ExtendedDataOutput out, String value) throws IOException { synchronized (this) { ++stringsWritten; } super.writeString(out, value); } } }