All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.samskivert.depot.Transformers Maven / Gradle / Ivy

//
// Depot library - a Java relational persistence library
// https://github.com/threerings/depot/blob/master/LICENSE

package com.samskivert.depot;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;

import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Transform;
import com.samskivert.depot.util.ByteEnum;

import static com.samskivert.depot.Log.log;

/**
 * Contains various generally useful {@link Transformer} implementations. To use a transformer, you
 * specify it via a {@link Column} annotation. For example:
 * 
 * public class MyRecord extends PersistentRecord {
 *     @Transform(Transformers.StringArray.class)
 *     public String[] cities;
 * }
 * 
*/ public class Transformers { /** * Transform a Set of ByteEnums into an Integer. * Does not presently support the immutable or interning hints. */ public static class ByteEnumSet & ByteEnum> extends Transformer, Integer> { @Override @SuppressWarnings("unchecked") public void init (Type fieldType, Transform annotation) { _eclass = (Class) ((ParameterizedType) fieldType).getActualTypeArguments()[0]; } @Override public Integer toPersistent (Set value) { return (value == null) ? null : ByteEnum.Util.setToInt(value); } @Override public Set fromPersistent (Integer encoded) { return (encoded == null) ? null : ByteEnum.Util.intToSet(_eclass, encoded); } /** The enum class token. */ protected Class _eclass; } /** * Combines the contents of a String[] column into a single String, terminating each String * element with a newline. A backslash ('\') in Strings will be prefixed by another backslash, * newlines will be encoded as "\n", and null elements will be encoded as "\0" (but not * terminated by a newline). */ public static class StringArray extends StringBase { @Override protected Iterable asIterable (String[] value) { return Arrays.asList(value); } @Override protected Builder createBuilder (String encoded) { // jog through and count the elements so that we can populate the array directly final String[] result = new String[countElements(encoded)]; return new Builder() { public void add (String s) { result[idx++] = s; } public String[] build () { return result; } protected int idx = 0; }; } } /** * Combines the contents of a {@code List/Set/Collection/Iterable} column into a single * String, terminating each String element with a newline. Null-tolerant. A backslash ('\') in * Strings will be prefixed by another backslash, newlines will be encoded as "\n", and null * elements will be encoded as "\0" (but not terminated by a newline). */ public static class StringIterable extends StringBase> { @Override protected Iterable asIterable (Iterable value) { return value; } @Override protected Builder> createBuilder (String encoded) { return createCollectionBuilder(_ftype, String.class, _immutable, _intern, -1); } } /** * A Transformer for anything that is an {@code Iterable}. Often used for Enum sets. */ public static class EnumIterable> extends StringBase> { @Override @SuppressWarnings("unchecked") public void init (Type fieldType, Transform annotation) { super.init(fieldType, annotation); _eclass = (Class) ((ParameterizedType) fieldType).getActualTypeArguments()[0]; _internStrings = false; // don't waste time interning strings } @Override protected Iterable asIterable (Iterable value) { return Iterables.transform(value, new Function() { public String apply (E val) { return (val == null) ? null : val.name(); } }); } @Override protected Builder> createBuilder (String encoded) { // create a Builder for our field type final Builder> ebuilder = createCollectionBuilder( _ftype, hasNullElement(encoded) ? null : _eclass, _immutable, _intern, -1); // wrap that builder in one that accepts String elements return new Builder>() { public void add (String s) { E value; if (s == null) { value = null; } else { try { value = Enum.valueOf(_eclass, s); } catch (IllegalArgumentException iae) { log.warning("Invalid enum cannot be unpersisted", "e", s, iae); return; } } ebuilder.add(value); } public Iterable build () { return ebuilder.build(); } }; } /** The enum class. */ protected Class _eclass; } /** * Can transform a List/Set/Collection/Iterable to an int[] for storage in the db. * Null-tolerant. */ public static class IntegerIterable extends Transformer, int[]> { @Override public void init (Type fieldType, Transform annotation) { _ftype = fieldType; _immutable = annotation.immutable(); _intern = annotation.intern(); } @Override public int[] toPersistent (Iterable itr) { if (itr == null) { return null; } Collection coll = (itr instanceof Collection) ? (Collection)itr : Lists.newArrayList(itr); return Ints.toArray(coll); } @Override public Iterable fromPersistent (int[] value) { if (value == null) { return null; } Builder> builder = createCollectionBuilder(_ftype, Integer.class, _immutable, _intern, value.length); for (int v : value) { builder.add(v); } return builder.build(); } /** The type of the field. */ protected Type _ftype; /** Immutable hint. */ protected boolean _immutable; /** Intern hint. */ protected boolean _intern; } /** * An interface used by some of these transformers to build their result. */ protected interface Builder { void add (E element); B build (); } /** * Create a builder that populates a collection. * * @param elementType if non-null and an enum, will be used to possibly create an EnumSet. * @param sizeHint -1 or an *exact size* hint. */ protected static Builder> createCollectionBuilder ( Type fieldType, Class elementType, boolean immutable, boolean intern, int sizeHint) { Collection adder; Collection retval = null; Type clazz = (fieldType instanceof ParameterizedType) ? ((ParameterizedType)fieldType).getRawType() : fieldType; // TODO: fill out the collection types // TODO: also, it might be nice to build into an ImmutableCollection if making // immutable (instead of wrapping in a unmodifiableCollection), but that results in // an extra copy... Perhaps we can add another flag that opts-in to that? Aiya. if (clazz == HashSet.class || clazz == Set.class || clazz == EnumSet.class) { Set set; if (clazz == HashSet.class || (elementType == null) || !elementType.isEnum()) { Preconditions.checkArgument(clazz != EnumSet.class, "Cannot proceed: EnumSet field is to be populated with a null element."); set = (sizeHint < 0) ? Sets.newHashSet() : Sets.newHashSetWithExpectedSize(sizeHint); } else { @SuppressWarnings({ "unchecked", "rawtypes" }) Class eclazz = (Class)elementType; @SuppressWarnings("unchecked") Set eSet = EnumSet.noneOf(eclazz); set = eSet; } adder = set; if (immutable && (clazz == Set.class)) { retval = Collections.unmodifiableSet(set); } } else if (clazz == LinkedList.class) { adder = Lists.newLinkedList(); } else { List list = (sizeHint < 0) ? Lists.newArrayList() : Lists.newArrayListWithCapacity(sizeHint); adder = list; if (immutable && ((clazz == List.class) || (clazz == Iterable.class) || (clazz == Collection.class))) { retval = Collections.unmodifiableList(list); } } // ok, now we're ready final Collection fadder = adder; final Collection fretval = (retval == null) ? adder : retval; final boolean fintern = (retval != null) && immutable && intern; return new Builder>() { public void add (E element) { fadder.add(element); } public Iterable build () { if (fintern) { Object interned = INTERNER.intern(fretval); @SuppressWarnings("unchecked") Iterable built = (Iterable)interned; return built; } return fretval; } }; } /** * Building-block used to create other Transformers. */ protected abstract static class StringBase extends Transformer { @Override public void init (Type fieldType, Transform annotation) { _ftype = fieldType; _immutable = annotation.immutable(); _internStrings = _intern = annotation.intern(); } @Override public String toPersistent (F value) { if (value == null) { return null; } StringBuilder buf = new StringBuilder(); for (String s : asIterable(value)) { if (s == null) { buf.append("\\\n"); // encode nulls as slash followed by the terminator } else { s = s.replace("\\", "\\\\"); // turn \ into \\ s = s.replace("\n", "\\n"); // turn a newline in a String to "\n" buf.append(s).append('\n'); } } return buf.toString(); } @Override public F fromPersistent (String encoded) { if (encoded == null) { return null; } Builder builder = createBuilder(encoded); StringBuilder buf = new StringBuilder(encoded.length()); for (int ii = 0, nn = encoded.length(); ii < nn; ii++) { char c = encoded.charAt(ii); switch (c) { case '\n': String s = buf.toString(); builder.add(_internStrings ? s.intern() : s); buf.setLength(0); break; case '\\': Preconditions.checkArgument(++ii < nn, "Invalid encoded string"); char slashed = encoded.charAt(ii); switch (slashed) { case '\n': // turn back into a null element Preconditions.checkArgument(buf.length() == 0, "Invalid encoded string"); builder.add(null); break; case 'n': // turn \n back into a newline buf.append('\n'); break; default: // this should only be a slash... buf.append(slashed); break; } break; default: buf.append(c); break; } } // make sure the last element was terminated Preconditions.checkArgument(buf.length() == 0, "Invalid encoded string"); return builder.build(); } protected abstract Iterable asIterable (F value); protected abstract Builder createBuilder (String encoded); /** * Utility to count the number of elements in the encoded non-null string. */ protected static int countElements (String encoded) { int count = 0; for (int pos = 0; 0 != (pos = 1 + encoded.indexOf('\n', pos)); count++) {} return count; } /** * Utility to see if there are any nulls in the encoded string. */ protected static boolean hasNullElement (String encoded) { for (int pos = 0; -1 != (pos = encoded.indexOf("\\\n", pos)); pos += 2) { // make sure there isn't another slash before this token if ((pos == 0) || ('\\' != encoded.charAt(pos - 1))) { return true; } } return false; } protected Type _ftype; /** Immutable hint. */ protected boolean _immutable; /** Interning hint.*/ protected boolean _intern; /** Do we intern the actual strings? */ protected boolean _internStrings; } /** The interner we use for immutable values. */ protected static final Interner INTERNER = Interners.newWeakInterner(); }