/** * 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.api.resourcemodels.ResourceModelPostProcessor; import io.neba.core.resourcemodels.metadata.MappedFieldMetaData; import io.neba.core.resourcemodels.metadata.ResourceModelMetaData; import io.neba.core.resourcemodels.metadata.ResourceModelMetaDataRegistrar; import io.neba.core.util.OsgiBeanSource; import org.apache.sling.api.resource.Resource; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.Advised; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import static java.lang.System.currentTimeMillis; import static org.apache.commons.lang.StringUtils.join; import static org.springframework.util.Assert.notNull; /** * Maps the properties of a {@link Resource} onto a {@link io.neba.api.annotations.ResourceModel} using * the {@link FieldValueMappingCallback}. Applies the registered * {@link ResourceModelPostProcessor post processors} to the model before and * after the fields are mapped. * * @author Olaf Otto */ @Service public class ResourceToModelMapper { private final List<ResourceModelPostProcessor> postProcessors = new ArrayList<>(); @Autowired private ModelProcessor modelProcessor; @Autowired private NestedMappingSupport nestedMappingSupport; @Autowired private AnnotatedFieldMappers annotatedFieldMappers; @Autowired private ResourceModelMetaDataRegistrar resourceModelMetaDataRegistrar; /** * @param resource must not be <code>null</code>. * @param modelSource must not be <code>null</code>. * @param <T> the bean type. * @return never <code>null</code>. */ public <T> T map(final Resource resource, final OsgiBeanSource<T> modelSource) { notNull(resource, "Method argument resource must not be null."); notNull(modelSource, "Method argument modelSource must not be null."); T model = null; final Class<?> beanType = modelSource.getBeanType(); final ResourceModelMetaData metaData = this.resourceModelMetaDataRegistrar.get(beanType); final Mapping<T> mapping = new Mapping<>(resource.getPath(), metaData); // Do not track mapping time for nested resource models of the same type: this would yield // a useless average and total mapping time as the mapping durations would sum up multiple times. final boolean trackMappingDuration = !this.nestedMappingSupport.hasOngoingMapping(metaData); final Mapping<T> alreadyOngoingMapping = this.nestedMappingSupport.begin(mapping); if (alreadyOngoingMapping == null) { try { // Phase 1: Obtain bean instance. All standard bean lifecycle phases (such as @PostConstruct) // and processors are executed during this invocation. final T bean = modelSource.getBean(); metaData.getStatistics().countInstantiation(); // Phase 2: Retain the bean prior to mapping in order to return it if the mapping results in a cycle. mapping.setMappedModel(bean); // Phase 3: Map the bean (may create a cycle). // Retain current time for statistics final long startTimeInMs = trackMappingDuration ? currentTimeMillis() : 0; model = map(resource, bean, metaData, modelSource.getFactory()); if (trackMappingDuration) { // Update statistics with mapping duration metaData.getStatistics().countMappingDuration((int) (currentTimeMillis() - startTimeInMs)); } } finally { this.nestedMappingSupport.end(mapping); } } else { // Yield the currently mapped bean. model = alreadyOngoingMapping.getMappedModel(); if (model == null) { // This can only be the case if a cycle was introduced during phase 1. // Cycles introduced during bean initialization in the bean factory always // represent unresolvable programming errors (the bean depends on itself to initialize itself), // thus we must raise an exception. throw new CycleInBeanInitializationException("Unable to provide bean " + beanType + " for resource " + resource + ". The bean initialization resulted in a cycle: " + join(this.nestedMappingSupport.getOngoingMappings(), " >> ") + " >> " + mapping + ". " + "Does the bean depend on itself to initialize, e.g. in a @PostConstruct method?"); } } return model; } @SuppressWarnings("unchecked") private <T> T getTargetObjectOfAdvisedBean(Advised bean) { TargetSource targetSource = bean.getTargetSource(); if (targetSource == null) { throw new IllegalStateException("Model " + bean + " is " + Advised.class.getName() + ", but its target source is null."); } Object target; try { target = targetSource.getTarget(); } catch (Exception e) { throw new IllegalStateException("Unable to obtain the target of the advised model " + bean + ".", e); } if (target == null) { throw new IllegalStateException("The advised target of bean " + bean + " must not be null."); } return (T) target; } private <T> T map(final Resource resource, final T bean, final ResourceModelMetaData metaData, final BeanFactory factory) { T preprocessedModel = preProcess(resource, bean, factory); T model = preprocessedModel; // Unwrap proxied beans prior to mapping. The mapping must access the target // bean's fields in order to perform value injection there. if (preprocessedModel instanceof Advised) { model = getTargetObjectOfAdvisedBean((Advised) bean); } final FieldValueMappingCallback callback = new FieldValueMappingCallback(model, resource, factory, this.annotatedFieldMappers); for (MappedFieldMetaData mappedFieldMetaData : metaData.getMappableFields()) { callback.doWith(mappedFieldMetaData); } // Do not expose the unwrapped model to the post processors, use the proxy (if any) instead. return postProcess(resource, preprocessedModel, factory); } private <T> T preProcess(final Resource resource, final T model, final BeanFactory factory) { final ResourceModelMetaData metaData = this.resourceModelMetaDataRegistrar.get(model.getClass()); this.modelProcessor.processBeforeMapping(metaData, model); T currentModel = model; for (ResourceModelPostProcessor processor : this.postProcessors) { T processedModel = processor.processBeforeMapping(currentModel, resource, factory); if (processedModel != null) { currentModel = processedModel; } } return currentModel; } private <T> T postProcess(final Resource resource, final T model, final BeanFactory factory) { final ResourceModelMetaData metaData = this.resourceModelMetaDataRegistrar.get(model.getClass()); this.modelProcessor.processAfterMapping(metaData, model); T currentModel = model; for (ResourceModelPostProcessor processor : this.postProcessors) { T processedModel = processor.processAfterMapping(currentModel, resource, factory); if (processedModel != null) { currentModel = processedModel; } } return currentModel; } public void bind(ResourceModelPostProcessor postProcessor) { this.postProcessors.add(postProcessor); } public void unbind(ResourceModelPostProcessor postProcessor) { if (postProcessor == null) { return; } this.postProcessors.remove(postProcessor); } }