/* * * * Copyright (c) 2016. David Sowerby * * * * 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 uk.q3c.krail.core.i18n; import com.google.inject.Inject; import com.vaadin.ui.AbstractComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.*; import static com.google.common.base.Preconditions.checkNotNull; /** * Created by David Sowerby on 10/05/15. */ public class DefaultI18NFieldScanner implements I18NFieldScanner { private static Logger log = LoggerFactory.getLogger(DefaultI18NFieldScanner.class); private Map<AbstractComponent, AnnotationInfo> components; private LinkedList<Object> drillDowns; private I18NHostClassIdentifier i18NHostClassIdentifier; private List<Object> processedDrillDowns; @Inject protected DefaultI18NFieldScanner(I18NHostClassIdentifier i18NHostClassIdentifier) { this.i18NHostClassIdentifier = i18NHostClassIdentifier; components = new HashMap<>(); drillDowns = new LinkedList<>(); processedDrillDowns = new ArrayList<>(); } @Override @Nonnull public Map<AbstractComponent, AnnotationInfo> annotatedComponents() { return components; } /** * Scans the class of target for I18N annotated fields, working up the inheritance tree. Uses {@link #i18NHostClassIdentifier} to resolve a class which * has * been enhanced * * @param target */ @Override public void scan(@Nonnull Object target) { checkNotNull(target); drillDowns.clear(); processedDrillDowns.clear(); components.clear(); drillDowns.add(target); doScan(target); } /** * @param target */ protected void doScan(Object target) { Class<?> classToScan = i18NHostClassIdentifier.getOriginalClassFor(target); log.debug("scanning '{}' for I18N Annotations", classToScan.getName()); try { while (!classToScan.equals(Object.class)) { findAnnotatedFields(classToScan.getDeclaredFields(), target); classToScan = classToScan.getSuperclass(); } } catch (IllegalAccessException iae) { log.error("I18N scan failed", iae); } processedDrillDowns.add(target); drillDowns.remove(target); while (!drillDowns.isEmpty()) { Object next = drillDowns.getFirst(); //beware duplicates, they will cause stack overflow if (processedDrillDowns.contains(next)) { drillDowns.remove(next); } else { doScan(next); } } } /** * Fields are included for processing if they have one or more annotations which is itself annotated with {@link I18NAnnotation}. Another list is * constructed for components which the processor should drill down into. * <p> * Fields are included for drill down if they are:<ol><li>annotated with {@link I18N} with value=true (the default) * * @param declaredFields * declared fields, taken from a class in the overall hierarchy of {@code target} */ private void findAnnotatedFields(Field[] declaredFields, Object target) throws IllegalAccessException { for (Field field : declaredFields) { log.debug("Capture all field and class annotations for '{}' ", field.getName()); Annotation[] fieldAnnotations = field.getDeclaredAnnotations(); Annotation[] classAnnotations = field.getType() .getDeclaredAnnotations(); //do field annotations first, we can ignore class annotations if there are field annotations boolean found = evaluateFieldFromAnnotations(target, field, fieldAnnotations); log.debug("I18N field annotations found, class annotations will be ignored"); if (!found) { evaluateFieldFromAnnotations(target, field, classAnnotations); } evaluateDrillDown(target, field); } } /** * @param field * the field being processed * @param annotations * the annotations associated with the field, whether they came from the field itself, or the class of its type. * * @throws IllegalAccessException */ private boolean evaluateFieldFromAnnotations(Object target, Field field, Annotation[] annotations) throws IllegalAccessException { log.debug("evaluating annotations for field '{}'", field.getName()); AnnotationInfo annotationInfo = new AnnotationInfo(field); // identify any I18N annotations for the component, but exclude the I18N Drill down for (Annotation annotation : annotations) { //is it an I18N annotation? if (annotation.annotationType() .isAnnotationPresent(I18NAnnotation.class)) { log.debug("Annotation @{} found for field '{}'", annotation.annotationType(), field.getName()); //we don't want I18N in this list, it is used to include / exclude drill down if (!annotation.annotationType() .equals(I18N.class)) { annotationInfo.getAnnotations() .add(annotation); } } } if (annotationInfo.getAnnotations() .isEmpty()) { log.debug("No I18N annotations found for '{}'", field.getName()); return false; } else { if (AbstractComponent.class.isAssignableFrom(field.getType())) { field.setAccessible(true); AbstractComponent abstractComponent = (AbstractComponent) field.get(target); if (abstractComponent != null) { components.put(abstractComponent, annotationInfo); } log.debug("I18N annotation(s) found for '{}', added to components list", field.getName()); return true; } else { throw new I18NException("I18N annotations (except for @18N), can only be applied to AbstractComponent"); } } } private void evaluateDrillDown(Object target, Field field) throws IllegalAccessException { //try field annotations first; // if I18N(drillDown = true, just add it to drill downs) I18N i18N = field.getAnnotation(I18N.class); if (i18N != null) { log.debug("evaluating '{}' for @I18N field annotation drill down", field.getName()); if (i18N.drillDown()) { addDrillDown(target, field); log.debug("'{}' has field annotation @18N(drillDown=true), added to drill downs", field.getName()); return; } else { log.debug("'{}' has field annotation @18N(drillDown=false), not added to drill downs", field.getName()); return; } } log.debug("No @I18N field annotation found for '{}', check its class for @18N drill down", field.getName()); Class<?> fieldType = field.getType(); i18N = fieldType.getAnnotation(I18N.class); if (i18N != null) { if (i18N.drillDown()) { addDrillDown(target, field); log.debug("'{}' has class annotation @18N(drillDown=true), added to drill downs", field.getName()); return; } else { log.debug("'{}' has class annotation @18N(drillDown=false), not added to drill downs", field.getName()); return; } } } private void addDrillDown(Object target, Field field) throws IllegalAccessException { field.setAccessible(true); Object component = field.get(target); if (component != null) { drillDowns.add(component); log.debug("'{}' included for drill down", field.getName()); } else { log.debug("'{}' is null, not added to drill downs", field.getName()); } } /** * Returns all the objects that were drilled into, including the inital target submitted to {@link #scan} * * @return all the objects that were drilled into, including the inital target submitted to {@link #scan} */ @Override public List<Object> processedDrillDowns() { return processedDrillDowns; } }