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