/** * Copyright 2013 the original author or authors. * * 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 io.neba.core.resourcemodels.mapping; import io.neba.api.resourcemodels.AnnotatedFieldMapper; import io.neba.core.resourcemodels.metadata.MappedFieldMetaData; import io.neba.core.util.ConcurrentDistinctMultiValueMap; import org.springframework.stereotype.Service; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import static java.util.Collections.emptyList; import static org.apache.commons.lang.ClassUtils.primitiveToWrapper; /** * Represents all registered {@link io.neba.api.resourcemodels.AnnotatedFieldMapper custom field mappers} * and provides the corresponding lookup and caching of lookup results. * * @author Olaf Otto */ @Service public class AnnotatedFieldMappers { /** * Represents the relation of an {@link java.lang.annotation.Annotation} and and a * {@link io.neba.api.resourcemodels.AnnotatedFieldMapper#getAnnotationType() compatible mapper}. * * @author Olaf Otto */ public static class AnnotationMapping { private final Annotation annotation; private final AnnotatedFieldMapper mapper; public AnnotationMapping(Annotation annotation, AnnotatedFieldMapper mapper) { this.annotation = annotation; this.mapper = mapper; } public Annotation getAnnotation() { return annotation; } public AnnotatedFieldMapper getMapper() { return mapper; } } private static final Collection<AnnotationMapping> EMPTY = emptyList(); private final ConcurrentDistinctMultiValueMap<Field, AnnotationMapping> cache = new ConcurrentDistinctMultiValueMap<>(); private final ConcurrentDistinctMultiValueMap<Class<? extends Annotation>, AnnotatedFieldMapper> fieldMappers = new ConcurrentDistinctMultiValueMap<>(); private final AtomicInteger state = new AtomicInteger(0); public synchronized void bind(AnnotatedFieldMapper<?, ?> mapper) { if (mapper == null) { throw new IllegalArgumentException("Method argument mapper must not be null."); } this.state.getAndIncrement(); this.fieldMappers.put(mapper.getAnnotationType(), mapper); this.cache.clear(); } /** * @param mapper must not be <code>null</code>. */ public synchronized void unbind(AnnotatedFieldMapper<?, ?> mapper) { if (mapper == null) { return; } this.state.getAndIncrement(); this.fieldMappers.removeValue(mapper); this.cache.clear(); } /** * @param metaData must not be <code>null</code>. * @return never <code>null</code> but rather an empty collection. */ public Collection<AnnotationMapping> get(MappedFieldMetaData metaData) { if (metaData == null) { throw new IllegalArgumentException("Method argument metaData must not be null."); } Collection<AnnotationMapping> mappers = this.cache.get(metaData.getField()); if (mappers != null) { return mappers; } final int ticket = this.state.get(); List<AnnotationMapping> compatibleMappers = new ArrayList<>(); for (Annotation annotation : metaData.getAnnotations()) { Collection<AnnotatedFieldMapper> mappersForAnnotation = this.fieldMappers.get(annotation.annotationType()); if (mappersForAnnotation == null) { continue; // with next element } for (AnnotatedFieldMapper<?, ?> mapper: mappersForAnnotation) { // Mappers supporting boxed types shall also support the primitive equivalent, // e.g. Boolean and boolean, Integer / int. Class type = primitiveToWrapper(metaData.getType()); if (mapper.getFieldType().isAssignableFrom(type)) { compatibleMappers.add(new AnnotationMapping(annotation, mapper)); } } } mappers = compatibleMappers.isEmpty() ? EMPTY : compatibleMappers; synchronized (this) { if (ticket == this.state.get()) { this.cache.put(metaData.getField(), mappers); } } return mappers; } }