blob: e44cfe3bcc2b8687faedf65d107b7d0ced8a6fbf [file] [log] [blame]
/*
* 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);
}
}
}