001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.kernel.impl.identifiers;
017
018import static com.google.common.base.Joiner.on;
019import static com.google.common.base.Splitter.fixedLength;
020import static com.google.common.collect.Iterables.concat;
021import static com.google.common.collect.Iterables.getLast;
022import static com.google.common.collect.Lists.transform;
023import static java.util.Arrays.asList;
024import static java.util.Collections.emptyList;
025import static java.util.Collections.singletonList;
026import static java.util.UUID.randomUUID;
027import static org.slf4j.LoggerFactory.getLogger;
028
029import java.util.List;
030
031import org.fcrepo.kernel.identifiers.InternalIdentifierConverter;
032import org.slf4j.Logger;
033
034import com.google.common.base.Function;
035
036/**
037 * Injects and extracts segments of hierarchy before the last segment of a
038 * multi-part identifier to ensure efficient performance of the JCR.
039 *
040 * @author ajs6f
041 * @since Mar 26, 2014
042 */
043public class HierarchyConverter extends InternalIdentifierConverter {
044
045    public static final String DEFAULT_SEPARATOR = "/";
046
047    private String separator = DEFAULT_SEPARATOR;
048
049    private String prefix = "";
050
051    private final Function<String, String> addPrefix = new Function<String, String>() {
052
053        @Override
054        public String apply(final String input) {
055            return prefix + input;
056        }
057    };
058
059    private static final int DEFAULT_LENGTH = 2;
060
061    private static final int DEFAULT_COUNT = 4;
062
063    private int length = DEFAULT_LENGTH;
064
065    private int levels = DEFAULT_COUNT;
066
067    private static final Logger log = getLogger(HierarchyConverter.class);
068
069    /*
070     * (non-Javadoc)
071     * @see com.google.common.base.Converter#doBackward(java.lang.Object)
072     */
073    @Override
074    protected String doBackward(final String flat) {
075        log.debug("Converting incoming identifier: {}", flat);
076        final List<String> hierarchySegments = createHierarchySegments();
077        final List<String> flatSegments = asList(flat.split(separator));
078        List<String> firstSegments = emptyList();
079        List<String> lastSegment = emptyList();
080        if (flatSegments.size() == 0) {
081            // either empty identifier or separator identifier
082            return on(separator).join(hierarchySegments);
083        }
084        if (flatSegments.size() > 1) {
085            lastSegment = singletonList(getLast(flatSegments));
086            firstSegments = flatSegments.subList(0, flatSegments.size() - 1);
087        } else {
088            // just one segment
089            lastSegment = singletonList(flatSegments.get(0));
090        }
091        final Iterable<String> allSegments = concat(firstSegments, hierarchySegments, lastSegment);
092
093        return on(separator).join(allSegments);
094    }
095
096    /*
097     * (non-Javadoc)
098     * @see com.google.common.base.Converter#doForward(java.lang.Object)
099     */
100    @Override
101    protected String doForward(final String hierarchical) {
102        log.debug("Converting outgoing identifier: {}", hierarchical);
103        final List<String> segments = asList(hierarchical.split(separator));
104        if (segments.size() <= levels) {
105            // must be a root identifier
106            return "";
107        }
108        List<String> firstSegments = emptyList();
109        List<String> lastSegment = emptyList();
110        if (segments.size() > levels + 1) {
111            // we subtract one for the final segment, then levels for the
112            // inserted hierarchy segments we want to remove
113            firstSegments = segments.subList(0, segments.size() - 1 - levels);
114            lastSegment = singletonList(getLast(segments));
115        } else {
116            // just the trailing non-hierarchical segment
117            lastSegment = singletonList(getLast(segments));
118        }
119        return on(separator).join(concat(firstSegments, lastSegment));
120    }
121
122    private List<String> createHierarchySegments() {
123        // offers a list of segments sliced out of a UUID, each prefixed
124        if (levels == 0) {
125            return emptyList();
126        }
127        return transform(fixedLength(length).splitToList(createHierarchyCharacterBlock()), addPrefix);
128    }
129
130    private CharSequence createHierarchyCharacterBlock() {
131        return randomUUID().toString().replace("-", "").substring(0, length * levels);
132    }
133
134    /**
135     * @param sep the separator to use
136     */
137    public void setSeparator(final String sep) {
138        this.separator = sep;
139    }
140
141    /**
142     * @param l the length to set
143     */
144    public void setLength(final int l) {
145        if (l < 1) {
146            throw new IllegalArgumentException("Segment length must be at least one!");
147        }
148        this.length = l;
149    }
150
151    /**
152     * @param l the levels to set
153     */
154    public void setLevels(final int l) {
155        this.levels = l;
156    }
157
158    /**
159     * @param p the prefix to set
160     */
161    public void setPrefix(final String p) {
162        this.prefix = p;
163    }
164
165}