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.modeshape.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.api.identifiers.InternalIdentifierConverter;
032import org.slf4j.Logger;
033
034/**
035 * Injects and extracts segments of hierarchy before the last segment of a
036 * multi-part identifier to ensure efficient performance of the JCR.
037 *
038 * @author ajs6f
039 * @since Mar 26, 2014
040 */
041public class HierarchyConverter extends InternalIdentifierConverter {
042
043    public static final String DEFAULT_SEPARATOR = "/";
044
045    private String separator = DEFAULT_SEPARATOR;
046
047    private String prefix = "";
048
049    private static final int DEFAULT_LENGTH = 2;
050
051    private static final int DEFAULT_COUNT = 4;
052
053    private int length = DEFAULT_LENGTH;
054
055    private int levels = DEFAULT_COUNT;
056
057    private static final Logger log = getLogger(HierarchyConverter.class);
058
059    /*
060     * (non-Javadoc)
061     * @see com.google.common.base.Converter#doBackward(java.lang.Object)
062     */
063    @Override
064    protected String doBackward(final String flat) {
065        log.debug("Converting incoming identifier: {}", flat);
066        final List<String> hierarchySegments = createHierarchySegments();
067        final List<String> flatSegments = asList(flat.split(separator));
068        List<String> firstSegments = emptyList();
069        List<String> lastSegment = emptyList();
070        if (flatSegments.size() == 0) {
071            // either empty identifier or separator identifier
072            return on(separator).join(hierarchySegments);
073        }
074        if (flatSegments.size() > 1) {
075            lastSegment = singletonList(getLast(flatSegments));
076            firstSegments = flatSegments.subList(0, flatSegments.size() - 1);
077        } else {
078            // just one segment
079            lastSegment = singletonList(flatSegments.get(0));
080        }
081        final Iterable<String> allSegments = concat(firstSegments, hierarchySegments, lastSegment);
082
083        return on(separator).join(allSegments);
084    }
085
086    /*
087     * (non-Javadoc)
088     * @see com.google.common.base.Converter#doForward(java.lang.Object)
089     */
090    @Override
091    protected String doForward(final String hierarchical) {
092        log.debug("Converting outgoing identifier: {}", hierarchical);
093        final List<String> segments = asList(hierarchical.split(separator));
094        if (segments.size() <= levels) {
095            // must be a root identifier
096            return "";
097        }
098        List<String> firstSegments = emptyList();
099        List<String> lastSegment = emptyList();
100        if (segments.size() > levels + 1) {
101            // we subtract one for the final segment, then levels for the
102            // inserted hierarchy segments we want to remove
103            firstSegments = segments.subList(0, segments.size() - 1 - levels);
104            lastSegment = singletonList(getLast(segments));
105        } else {
106            // just the trailing non-hierarchical segment
107            lastSegment = singletonList(getLast(segments));
108        }
109        return on(separator).join(concat(firstSegments, lastSegment));
110    }
111
112    private List<String> createHierarchySegments() {
113        // offers a list of segments sliced out of a UUID, each prefixed
114        if (levels == 0) {
115            return emptyList();
116        }
117        return transform(fixedLength(length).splitToList(createHierarchyCharacterBlock()), x -> prefix + x);
118    }
119
120    private CharSequence createHierarchyCharacterBlock() {
121        return randomUUID().toString().replace("-", "").substring(0, length * levels);
122    }
123
124    /**
125     * @param sep the separator to use
126     */
127    public void setSeparator(final String sep) {
128        this.separator = sep;
129    }
130
131    /**
132     * @param l the length to set
133     */
134    public void setLength(final int l) {
135        if (l < 1) {
136            throw new IllegalArgumentException("Segment length must be at least one!");
137        }
138        this.length = l;
139    }
140
141    /**
142     * @param l the levels to set
143     */
144    public void setLevels(final int l) {
145        this.levels = l;
146    }
147
148    /**
149     * @param p the prefix to set
150     */
151    public void setPrefix(final String p) {
152        this.prefix = p;
153    }
154
155}