blob: 0fc131c2904deb8bac5d38d9ceeee99fe51139e7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.sis.internal.jaxb;
import java.net.URI;
import java.util.Objects;
import java.util.Iterator;
import java.util.Collection;
import java.net.URISyntaxException;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.xml.IdentifierSpace;
import org.apache.sis.xml.ValueConverter;
import org.apache.sis.xml.XLink;
/**
* A map of identifiers which support {@code put} and {@code remove} operations.
*
* <h2>Thread safety</h2>
* This class is thread safe if the underlying identifier collection is thread safe.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.7
*
* @see org.apache.sis.xml.IdentifiedObject
*
* @since 0.7
* @module
*/
public final class ModifiableIdentifierMap extends IdentifierMapAdapter {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -80325787192055778L;
/**
* Creates a new map which will be a view over the given identifiers.
*
* @param identifiers the identifiers to wrap in a map view.
*/
public ModifiableIdentifierMap(final Collection<Identifier> identifiers) {
super(identifiers);
}
/**
* Sets the {@code xlink:href} value, which may be null. If an explicit {@code xlink:href} identifier exists,
* then it will removed before to set the new {@code href} in the {@link XLink} object. The intent is to give
* precedence to the {@link XLink#getHRef()} property in every cases where the {@code href} is parsable as a
* {@link URI}, and use the value associated to the {@code HREF} key only as a fallback when the string can not
* be parsed.
*
* @param href the new value, or {@code null} for removing the value.
* @return the previous value, or {@code null} if none.
*
* @see #getHRef()
*/
private URI setHRef(final URI href) {
URI old = store(IdentifierSpace.HREF, null);
final Identifier identifier = getIdentifier(IdentifierSpace.XLINK);
if (identifier instanceof SpecializedIdentifier<?>) {
final Object link = ((SpecializedIdentifier<?>) identifier).value;
if (link instanceof XLink) {
if (old == null) {
old = ((XLink) link).getHRef();
}
((XLink) link).setHRef(href);
return old;
}
}
if (href != null) {
final XLink link = new XLink();
link.setHRef(href);
store(IdentifierSpace.XLINK, link);
}
return old;
}
////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// END OF SPECIAL CASES. ////////
//////// ////////
//////// Implementation of IdentifierMap methods follow. Each method may ////////
//////// have a switch statement over the special cases declared above. ////////
//////// ////////
////////////////////////////////////////////////////////////////////////////////////////
/**
* Returns {@code true} since this map support {@code put} and {@code remove} operations.
*/
@Override
final boolean isModifiable() {
return true;
}
/**
* Removes every entries in the underlying collection.
*/
@Override
public void clear() {
identifiers.clear();
}
/**
* Removes all identifiers associated with the given {@linkplain Identifier#getAuthority() authority}.
* The default implementation delegates to {@link #put(Citation, String)} with a {@code null} value.
*
* @param authority the authority to search, which should be an instance of {@link Citation}.
* @return the code of the identifier for the given authority, or {@code null} if none.
*/
@Override
public String remove(final Object authority) {
return (authority instanceof Citation) ? put((Citation) authority, null) : null;
}
/**
* Sets the code of the identifier having the given authority to the given value.
* If no identifier is found for the given authority, a new one is created.
* If more than one identifier is found for the given authority, then all previous identifiers may be removed
* in order to ensure that the new entry will be the first entry, so it can be find by the {@code get} method.
*
* <p>If the given {@code authority} is {@code HREF} and if the given string is parsable as a {@link URI},
* then this method will actually store the value as the {@link XLink#getHRef()} property of the {@code XLink}
* associated to the {@code XLINK} key. Only if the given string can not be parsed, then the value is stored
* <cite>as-is</cite> under the {@code HREF} key.</p>
*
* @param authority the authority for which to set the code.
* @param code the new code for the given authority, or {@code null} for removing the entry.
* @return the previous code for the given authority, or {@code null} if none.
*/
@Override
public String put(final Citation authority, final String code) {
ArgumentChecks.ensureNonNull("authority", authority);
String previous = null;
Object discarded = null;
switch (specialCase(authority)) {
case NonMarshalledAuthority.HREF: {
URI uri = null;
if (code != null) {
final Context context = Context.current();
final ValueConverter converter = Context.converter(context);
try {
uri = converter.toURI(context, code);
} catch (URISyntaxException e) {
SpecializedIdentifier.parseFailure(context, code, URI.class, e);
discarded = setHRef(null);
break; // Fallback on generic code below.
}
}
final Identifier identifier = getIdentifier(authority);
uri = setHRef(uri);
if (uri != null) {
previous = uri.toString();
} else if (identifier != null) {
previous = identifier.getCode();
}
return previous;
}
// A future Apache SIS version may add more special cases here.
}
/*
* Generic code to be executed when the given authority is not one of the special case,
* or when it was a special case but parsing of the given string failed.
*/
final Iterator<? extends Identifier> it = identifiers.iterator();
while (it.hasNext()) {
final Identifier identifier = it.next();
if (identifier == null) {
it.remove(); // Opportunist cleaning, but should not happen.
} else if (Objects.equals(authority, identifier.getAuthority())) {
if (code != null && identifier instanceof IdentifierMapEntry) {
return ((IdentifierMapEntry) identifier).setValue(code);
/*
* No need to suppress other occurrences of the key (if any)
* because we made a replacement in the first entry, so the
* new value will be visible by the getter methods.
*/
}
if (previous == null) {
previous = identifier.getCode();
}
it.remove();
/*
* Continue the iteration in order to remove all other occurrences,
* in order to ensure that the getter methods will see the new value.
*/
}
}
if (code != null) {
identifiers.add(SpecializedIdentifier.parse(authority, code));
}
if (previous == null && discarded != null) {
previous = discarded.toString();
}
return previous;
}
/**
* Sets the identifier associated with the given authority, and returns the previous value.
*
* <p>If the given {@code authority} is {@code HREF}, then this method will actually store the value
* as the {@link XLink#getHRef()} property of the {@code XLink} associated to the {@code XLINK} key.
* The previous {@code HREF} value, if any, is discarded.</p>
*
* @param <T> the identifier type.
* @param authority the namespace with which the given identifier is to be associated.
* @param value the identifier to be associated with the given namespace.
* @return the previous identifier associated with {@code authority}, or {@code null}
* if there was no mapping of the specialized type for {@code authority}.
*/
@Override
@SuppressWarnings("unchecked")
public <T> T putSpecialized(final IdentifierSpace<T> authority, final T value) {
switch (specialCase(authority)) {
default: return store(authority, value);
case NonMarshalledAuthority.HREF: return (T) setHRef((URI) value);
// A future Apache SIS version may add more special cases here.
}
}
/**
* Sets the identifier associated with the given authority, without processing for special cases.
*
* @param <T> the identifier type.
* @param authority the namespace with which the given identifier is to be associated.
* @param value the identifier to be associated with the given namespace.
* @return the previous identifier associated with {@code authority}, or {@code null}
* if there was no mapping of the specialized type for {@code authority}.
*/
private <T> T store(final IdentifierSpace<T> authority, final T value) {
ArgumentChecks.ensureNonNull("authority", authority);
T old = null;
final Iterator<? extends Identifier> it = identifiers.iterator();
while (it.hasNext()) {
final Identifier identifier = it.next();
if (identifier == null) {
it.remove(); // Opportunist cleaning, but should not happen.
} else if (Objects.equals(authority, identifier.getAuthority())) {
if (identifier instanceof SpecializedIdentifier<?>) {
@SuppressWarnings("unchecked")
final SpecializedIdentifier<T> id = (SpecializedIdentifier<T>) identifier;
if (old == null) {
old = id.value;
}
if (value != null) {
id.value = value;
return old;
/*
* No need to suppress other occurrences of the key (if any)
* because we made a replacement in the first entry, so the
* new value will be visible by the getter methods.
*/
}
}
it.remove();
/*
* Continue the iteration in order to remove all other occurrences,
* in order to ensure that the getter methods will see the new value.
*/
}
}
if (value != null) {
identifiers.add(new SpecializedIdentifier<>(authority, value));
}
return old;
}
}