/*
* The MIT License
*
* Copyright (c) 2012, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.util;
import java.io.Serializable;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
/**
* {@link TreeString} is an alternative string representation that saves the
* memory when you have a large number of strings that share common prefixes
* (such as various file names.)
* <p>
* {@link TreeString} can be built with {@link TreeStringBuilder}.
*
* @author Kohsuke Kawaguchi
* @since 1.473
*/
// CHECKSTYLE:OFF
@SuppressWarnings("PMD")
public final class TreeString implements Serializable {
private static final long serialVersionUID = 3621959682117480904L;
/**
* Parent node that represents the prefix.
*/
private TreeString parent;
/**
* {@link #parent}+{@link #label} is the string value of this node.
*/
private char[] label;
/**
* Creates a new root {@link TreeString}
*/
/* package */TreeString() {
this(null, "");
}
/* package */TreeString(final TreeString parent, final String label) {
assert parent == null || label.length() > 0; // if there's a parent,
// label can't be empty.
this.parent = parent;
this.label = label.toCharArray(); // string created as a substring of
// another string can have a lot of
// garbage attached to it.
}
/* package */String getLabel() {
return new String(label);
}
/**
* Inserts a new node between this node and its parent, and returns the
* newly inserted node.
* <p>
* This operation doesn't change the string representation of this node.
*/
/* package */TreeString split(final String prefix) {
assert getLabel().startsWith(prefix);
char[] suffix = new char[label.length - prefix.length()];
System.arraycopy(label, prefix.length(), suffix, 0, suffix.length);
TreeString middle = new TreeString(parent, prefix);
label = suffix;
parent = middle;
return middle;
}
/**
* How many nodes do we have from the root to this node (including 'this'
* itself?) Thus depth of the root node is 1.
*/
private int depth() {
int i = 0;
for (TreeString p = this; p != null; p = p.parent) {
i++;
}
return i;
}
@Override
public boolean equals(final Object rhs) {
if (rhs == null) {
return false;
}
return rhs.getClass() == TreeString.class
&& ((TreeString)rhs).getLabel().equals(getLabel());
}
@Override
public int hashCode() {
int h = parent == null ? 0 : parent.hashCode();
for (int i = 0; i < label.length; i++) {
h = 31 * h + label[i];
}
assert toString().hashCode() == h;
return h;
}
/**
* Returns the full string representation.
*/
@Override
public String toString() {
char[][] tokens = new char[depth()][];
int i = tokens.length;
int sz = 0;
for (TreeString p = this; p != null; p = p.parent) {
tokens[--i] = p.label;
sz += p.label.length;
}
StringBuilder buf = new StringBuilder(sz);
for (char[] token : tokens) {
buf.append(token);
}
return buf.toString();
}
/**
* Interns {@link #label}
*/
/* package */void dedup(final Map<String, char[]> table) {
String l = getLabel();
char[] v = table.get(l);
if (v != null) {
label = v;
}
else {
table.put(l, label);
}
}
public boolean isBlank() {
return StringUtils.isBlank(toString());
}
public static String toString(final TreeString t) {
return t == null ? null : t.toString();
}
/**
* Creates a {@link TreeString}. Useful if you need to create one-off
* {@link TreeString} without {@link TreeStringBuilder}. Memory consumption
* is still about the same to {@code new String(s)}.
*
* @return null if the parameter is null
*/
public static TreeString of(final String s) {
if (s == null) {
return null;
}
return new TreeString(null, s);
}
/**
* Default {@link Converter} implementation for XStream that does interning
* scoped to one unmarshalling.
*/
@SuppressWarnings("all")
public static final class ConverterImpl implements Converter {
public ConverterImpl(final XStream xs) {}
public void marshal(final Object source, final HierarchicalStreamWriter writer,
final MarshallingContext context) {
writer.setValue(source == null ? null : source.toString());
}
public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
TreeStringBuilder builder = (TreeStringBuilder)context.get(TreeStringBuilder.class);
if (builder == null) {
context.put(TreeStringBuilder.class, builder = new TreeStringBuilder());
// dedup at the end
final TreeStringBuilder _builder = builder;
context.addCompletionCallback(new Runnable() {
public void run() {
_builder.dedup();
}
}, 0);
}
return builder.intern(reader.getValue());
}
public boolean canConvert(final Class type) {
return type == TreeString.class;
}
}
}