/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* 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 org.constellation.json.metadata;
import java.util.Arrays;
import java.io.IOException;
import org.apache.sis.util.CharSequences;
/**
* A path together with an index for each component in the path.
* Path indices begin at 1 (value 0 means "no index").
* This is used as keys for storing metadata values in a map.
*
* <p><b>Implementation note:</b>
* We need the full path, not only the last UML identifier, because the same metadata object could be reached
* by different paths (e.g. {@code "contact.party"} and {@code "identificationInfo.pointOfContact.party"}).</p>
*
* @author Martin Desruisseaux (Geomatys)
*/
final class NumerotedPath implements Comparable<NumerotedPath> {
/**
* The path elements.
*/
final String[] path;
/**
* The index of each elements in the path.
* Indices begin at 1. Value 0 means "no index".
*/
final int[] indices;
/**
* Creates new keys for the given node and indices.
*
* @param path The template containing the path.
* @param indices The index of each elements in the path.
*/
static NumerotedPath[] parse(final String[] path, final String indices) throws ParseException {
final CharSequence[] elements = CharSequences.split(indices, ',');
final NumerotedPath[] paths = new NumerotedPath[elements.length];
for (int i=0; i<paths.length; i++) {
paths[i] = new NumerotedPath(path, elements[i]);
}
return paths;
}
/**
* Creates a new key for the given node and indices specified in a space-separated list.
*
* @param path The template containing the path.
* @param indices The index of each elements in the path.
*/
private NumerotedPath(final String[] path, final CharSequence indices) throws ParseException {
this.path = path;
Throwable cause = null;
try {
this.indices = CharSequences.parseInts(indices, ' ', 10);
if (this.indices.length == path.length) {
return;
}
} catch (NumberFormatException e) {
cause = e;
}
throw new ParseException("Illegal indices: \"" + indices + "\".", cause);
}
/**
* Creates a new key for the given node and indices.
*
* @param path The template containing the path.
* @param indices The index of each elements in the path. This array will be partially copied.
*/
NumerotedPath(final String[] path, final int[] indices) {
this.path = path;
this.indices = Arrays.copyOfRange(indices, 0, path.length);
}
/**
* Invoked when the last index is irrelevant for us, except for differentiating singletons from collections.
*/
final void ignoreLastIndex() {
final int last = path.length - 1;
if (indices[last] != 0) {
indices[last] = 1;
}
}
/**
* Returns {@code true} if the values can be multiple.
*/
final boolean isMultiOccurrenceAllowed() {
return indices[indices.length - 1] != 0;
}
/**
* Returns a sub-path containing only the given number of components.
*/
final NumerotedPath head(final int depth) {
if (depth == path.length) {
return this;
}
return new NumerotedPath(Arrays.copyOfRange(path, 0, depth), indices);
}
/**
* Returns {@code true} if this path is a child of the given path.
*/
final boolean isChildOf(final NumerotedPath other) {
if (other != null) { // Special case for the needs of FormReader.writeMetadata.
if (path.length <= other.path.length) {
return false;
}
for (int i = other.path.length; --i >= 0;) {
if (!path[i].equals(other.path[i]) || indices[i] != other.indices[i]) {
return false;
}
}
}
return true;
}
/**
* Compares the given path with this one for order.
*/
@Override
public int compareTo(final NumerotedPath other) {
final int length = Math.min(path.length, other.path.length);
for (int i=0; i<length; i++) {
int c = path[i].compareTo(other.path[i]);
if (c == 0) {
c = indices[i] - other.indices[i];
if (c == 0) {
continue;
}
}
return c;
}
return path.length - other.path.length;
}
/**
* Returns {@code true} if the given key is equals to the given object.
*/
@Override
public boolean equals(final Object other) {
return (other instanceof NumerotedPath) &&
Arrays.equals(path, ((NumerotedPath) other).path) &&
Arrays.equals(indices, ((NumerotedPath) other).indices);
}
/**
* Returns a hash code value for this key.
*/
@Override
public int hashCode() {
return Arrays.hashCode(path) ^ Arrays.hashCode(indices);
}
/**
* Returns a string representation for debugging purpose.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder();
try {
formatPath(buffer, path, 0, indices);
} catch (IOException e) {
throw new AssertionError(e); // Should never happen, since we are writting to a StringBuilder.
}
return buffer.toString();
}
/**
* Formats the given path, without quotes.
*/
static void formatPath(final Appendable out, final CharSequence[] path, int pathOffset, final int[] indices) throws IOException {
while (pathOffset < path.length) {
if (pathOffset != 0) {
out.append(Keywords.PATH_SEPARATOR);
}
out.append(path[pathOffset]);
if (indices != null) {
final int index = indices[pathOffset];
if (index != 0) {
out.append('[').append(Integer.toString(index)).append(']');
}
}
pathOffset++;
}
}
}