/* * Copyright 2013 eBuddy B.V. * * 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 com.ebuddy.cassandra.structure; import static com.google.common.collect.Iterables.elementsEqual; import static com.google.common.collect.Iterables.getFirst; import static com.google.common.collect.Iterables.limit; import static com.google.common.collect.Iterables.skip; import static com.google.common.collect.Iterables.unmodifiableIterable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; import com.ebuddy.cassandra.Path; import com.google.common.base.Function; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; /** * Implementation of Path used as column names in Cassandra for encoding structures and for querying elements * of a structured object. * * @author Eric Zoerner <a href="mailto:ezoerner@ebuddy.com">ezoerner@ebuddy.com</a> */ public class DefaultPath implements Path { private static final char PATH_DELIMITER_CHAR = '/'; private static final String LIST_INDEX_PREFIX = "@"; private static final Function<String,String> urlEncodeFunction = new UrlEncode(); private final Iterable<String> pathElements; /** Create a DefaultPath from an iterable of encoded path element strings. */ private DefaultPath(Iterable<String> encodedPathElements) { pathElements = encodedPathElements; } /** Create a DefaultPath from un-encoded string elements. */ public static DefaultPath fromStrings(String... elements) { return new DefaultPath(Lists.transform(Arrays.asList(elements), urlEncodeFunction)); } @Override public Path concat(Path other) { Iterable<String> newPathElements = Iterables.concat(pathElements, other.getElements()); return new DefaultPath(newPathElements); } /** * Paths always end with the delimiter character in order to facilitate * start/finish slice queries in Cassandra. * @return the (encoded) String representation of a path. */ @Override public String toString() { StringBuilder builder = new StringBuilder(); for (String pathElement : pathElements) { builder.append(pathElement); builder.append(PATH_DELIMITER_CHAR); } return builder.toString(); } public static DefaultPath fromIndex(int i) { return new DefaultPath(Arrays.asList(LIST_INDEX_PREFIX + i)); } @Override public String head() { return getFirst(pathElements, null); } @Override public DefaultPath tail() { return tail(1); } @Override public DefaultPath tail(int startIndex) { return new DefaultPath(skip(pathElements, startIndex)); } @Override public boolean isEmpty() { return Iterables.isEmpty(pathElements); } @Override public boolean startsWith(Path path) { return elementsEqual(limit(pathElements, path.size()), path.getElements()); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } return elementsEqual(pathElements, ((DefaultPath)o).pathElements); } @Override public int hashCode() { return Lists.newArrayList(pathElements).hashCode(); } @Override public int size() { return Iterables.size(pathElements); } @Override public Iterable<String> getElements() { return unmodifiableIterable(pathElements); } @Override public Path withIndices(int... indices) { List<String> newPathElements = Lists.newArrayList(pathElements); for (int index : indices) { newPathElements.add(LIST_INDEX_PREFIX + index); } return new DefaultPath(newPathElements); } @Override public Path withElements(String... elements) { List<String> newPathElements = Lists.newArrayList(pathElements); newPathElements.addAll(Lists.transform(Arrays.asList(elements), urlEncodeFunction)); return new DefaultPath(newPathElements); } /** Create a Path object from a String produced by the Path#toString method. */ public static Path fromEncodedPathString(String pathString) { Iterable<String> parts = Splitter.on(PATH_DELIMITER_CHAR).omitEmptyStrings().split(pathString); return new DefaultPath(parts); } public static int getListIndex(String pathElement) { if (pathElement.isEmpty()) { throw new IllegalStateException("empty path"); } if (!pathElement.startsWith(LIST_INDEX_PREFIX)) { throw new IllegalStateException("not a list index"); } String rest = pathElement.substring(LIST_INDEX_PREFIX.length()); try { return Integer.parseInt(rest); } catch (NumberFormatException ignored) { throw new IllegalStateException("bad format for list index"); } } /** * Return true if all the given encoded path elements are list indexes. * @throws IllegalArgumentException if an empty path is found */ public static boolean isList(Iterable<String> encodedElements) { for (String element : encodedElements) { if (!isListIndex(element)) { return false; } } return true; } /** * Return true if the first element in this path is a list index. */ private static boolean isListIndex(String pathElement) { if (!pathElement.startsWith(LIST_INDEX_PREFIX)) { return false; } String rest = pathElement.substring(LIST_INDEX_PREFIX.length()); int index; try { index = Integer.parseInt(rest); } catch (NumberFormatException ignored) { return false; } return index >= 0; } private static class UrlEncode implements Function<String,String> { @Nullable @Override public String apply(@Nullable String s) { if (s == null) { return null; } String encodedString; try { encodedString = URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException ignored) { throw new AssertionError("UTF-8 is unknown?"); } return encodedString; } } }