/**
* Copyright 2013 the original author or authors.
* <p/>
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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 io.neba.core.resourcemodels.mapping;
import io.neba.core.resourcemodels.metadata.ResourceModelMetaData;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import static org.springframework.util.Assert.notNull;
/**
* Provides thread-local tracking of mapping invocations in order to support cycles in mappings
* and gather statistical data regarding mapping depths of
* {@link io.neba.api.annotations.ResourceModel resource models}.
*
* @author Olaf Otto
*/
@Service
@SuppressWarnings("rawtypes")
public class NestedMappingSupport {
/**
* Represents the stack of the currently ongoing mappings.
*
* @author Olaf Otto
*/
private static class OngoingMappings {
private final Map<Mapping, Mapping> mappings = new LinkedHashMap<>();
private final Map<ResourceModelMetaData, Count> metaData = new HashMap<>();
public void add(Mapping<?> mapping) {
this.mappings.put(mapping, mapping);
Count value = new Count();
Count previous = this.metaData.put(mapping.getMetadata(), value);
if (previous != null) {
value.add(previous);
}
}
public void remove(Mapping<?> mapping) {
this.mappings.remove(mapping);
Count count = this.metaData.get(mapping.getMetadata());
if (count != null && count.decrement() == 0) {
this.metaData.remove(mapping.getMetadata());
}
}
@SuppressWarnings("unchecked")
public <T> Mapping<T> get(Mapping<T> mapping) {
return this.mappings.get(mapping);
}
public boolean contains(ResourceModelMetaData metaData) {
return this.metaData.containsKey(metaData);
}
public boolean isEmpty() {
return this.mappings.isEmpty();
}
public Set<Mapping> getMappings() {
return this.mappings.keySet();
}
public Set<ResourceModelMetaData> getMetaData() {
return this.metaData.keySet();
}
/**
* A mutable key occurrence count in a map.
*
* @author Olaf Otto
*/
private static class Count {
private int i = 1;
public void add(Count other) {
i += other.i;
}
public int decrement() {
--i;
return i;
}
}
}
// Recursive mappings always occurs within the same thread.
private final ThreadLocal<OngoingMappings> ongoingMappings = new ThreadLocal<>();
/**
* Contract: When invoked and <code>null</code> is returned,
* one <em>must</em> invoke {@link #end(Mapping)} after the corresponding mapping was executed.<br />
* Otherwise, a leak in the form of persisting thread-local attributes is introduced.
*
* @param mapping must not be <code>null</code>.
* @return The already ongoing mapping, or <code>null</code> if the given mapping has not occurred
* yet. The provided mapping <em>must</em> only be executed if this emthod returns <code>null</code>.
* Otherwise, the execution results in an infinite loop.
*/
public <T> Mapping<T> begin(Mapping<T> mapping) {
notNull(mapping, "Method argument mapping must not be null.");
OngoingMappings ongoingMappings = getOrCreateMappings();
@SuppressWarnings("unchecked")
Mapping<T> alreadyExistingMapping = ongoingMappings.get(mapping);
if (alreadyExistingMapping == null) {
trackNestedMapping(ongoingMappings);
ongoingMappings.add(mapping);
}
return alreadyExistingMapping;
}
/**
* Ends a mapping that was {@link #begin(Mapping) begun}.
*
* @param mapping must not be <code>null</code>.
*/
public void end(Mapping mapping) {
notNull(mapping, "Method argument mapping must not be null.");
OngoingMappings ongoingMappings = this.ongoingMappings.get();
if (ongoingMappings != null) {
ongoingMappings.remove(mapping);
if (ongoingMappings.isEmpty()) {
this.ongoingMappings.remove();
}
}
}
/**
* @return a thread-local, ordered map representing the stack of currently ongoing mappings.
* Never <code>null</code> but rather an empty map.
*/
private OngoingMappings getOrCreateMappings() {
OngoingMappings ongoingMappings = this.ongoingMappings.get();
if (ongoingMappings == null) {
ongoingMappings = new OngoingMappings();
this.ongoingMappings.set(ongoingMappings);
}
return ongoingMappings;
}
/**
* Record a subsequent mapping in the {@link io.neba.core.resourcemodels.metadata.ResourceModelStatistics statistics}
* of every {@link ResourceModelMetaData resource model} in the current mapping stack.
*/
private void trackNestedMapping(OngoingMappings ongoingMappings) {
for (ResourceModelMetaData metaData : ongoingMappings.getMetaData()) {
metaData.getStatistics().countSubsequentMapping();
}
}
/**
* @return An unsafe view of the ongoing mappings. Modifications to the returned set will modify
* the state of this instance. Never null.
*/
public Set<Mapping> getOngoingMappings() {
return getOrCreateMappings().getMappings();
}
/**
* @param metadata must not be <code>null</code>.
* @return whether there is an ongoing (parent) mapping for the resource model represented by the provided
* meta data.
*/
public boolean hasOngoingMapping(ResourceModelMetaData metadata) {
notNull(metadata, "Method argument metadata must not be null.");
return getOrCreateMappings().contains(metadata);
}
}