Java tutorial
/* * * * 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.google.inject.Provider; import com.vaadin.data.HasValue; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.Grid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.q3c.krail.core.ui.ScopedUI; import uk.q3c.krail.i18n.CurrentLocale; import uk.q3c.krail.i18n.I18NException; import uk.q3c.krail.i18n.I18NKey; import uk.q3c.krail.i18n.I18NKeyConverter; import uk.q3c.krail.i18n.Translate; import uk.q3c.util.guice.SerializationSupport; import java.io.IOException; import java.io.ObjectInputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; /** * Utility class to manipulate Vaadin component settings to reflect locale changes. Class or field annotations can be used to specify the keys to use, and * this {@link I18NProcessor} implementation looks up the key values and sets caption, description and value properties of the component. * <p> * <p> * When a locale change occurs in {@link CurrentLocale}, {@link ScopedUI} updates itself and its current view. Other views, which may have already been * constructed, are updated as they become active. * <p> * <p>A {@link I18NFieldScanner} is used to read metadata (the annotations) from the target's fields * <p> * For a full description see https://sites.google.com/site/q3cjava/internationalisation-i18n * * @author David Sowerby 8 Feb 2013 */ public class DefaultI18NProcessor implements I18NProcessor { private static Logger log = LoggerFactory.getLogger(DefaultI18NProcessor.class); private final Translate translate; private CurrentLocale currentLocale; private transient Provider<I18NFieldScanner> i18NFieldScannerProvider; private SerializationSupport serializationSupport; @Inject protected DefaultI18NProcessor(CurrentLocale currentLocale, Translate translate, Provider<I18NFieldScanner> i18NFieldScannerProvider, SerializationSupport serializationSupport) { super(); this.currentLocale = currentLocale; this.translate = translate; this.i18NFieldScannerProvider = i18NFieldScannerProvider; this.serializationSupport = serializationSupport; } /** * Scans the {@code target} for fields with either a field or class I18N annotation. Caption, description and value are applied according to the value of * the combined annotations. Field annotations take precedence over class annotations, that is if there is any I18N field annotation, all I18N class * annotations are ignored. If there are multiple values for any given annotation method of caption(), description(), value() or locale(), the end * result will be one of the values supplied by an annotation but which one is indeterminate. * <p> * Drill down into a component (to look for nested components with I18N annotations) will occur only when a field or class is annotated with @I18N * * @param target the object to process for I18N annotation. If null, is just ignored */ @Override public void translate(Object target) { if (target == null) { return; } log.debug("scanning class '{}' for I18N annotations", target.getClass()); List<Object> processedFields = new ArrayList<Object>(); translate(processedFields, target); } /** * Translates {@code target} and keeps a running list of processed fields (or more accurately, the object contained by a Field). The latter is to ensure * that the same field is not evaluated twice - but is not only a waste of effort, but causes loops, where, for example a component contains a reference * to its parent. * <p> * Nulls are entirely valid if a Field has not been constructed, and are therefore just ignored. * * @param processedFields the fields already processed * @param target the field to be evaluated now */ protected void translate(List<Object> processedFields, Object target) { if (target == null) { return; } processedFields.add(target); I18NFieldScanner i18NFieldScanner = i18NFieldScannerProvider.get(); i18NFieldScanner.scan(target); try { processComponents(i18NFieldScanner.annotatedComponents(), target); } catch (Exception e) { throw new I18NException("I18N processing failed", e); } } protected void processComponents(Map<AbstractComponent, AnnotationInfo> componentAnnotations, Object target) { for (Map.Entry<AbstractComponent, AnnotationInfo> entry : componentAnnotations.entrySet()) { AbstractComponent component = entry.getKey(); AnnotationInfo annotationInfo = entry.getValue(); AnnotationValues annotationValues = annotationValues(annotationInfo.getAnnotations()); if (component instanceof Grid) { processGrid((Grid) component, annotationValues, annotationInfo); } else { applyAnnotationValues(component, annotationValues, annotationInfo); } } } /** * Returns a set of values from a list of annotations. There is no guarantee of evaluation order, so if a field has, for example, two annotations with a * caption() value, it is uncertain which will be selected * * @param annotations a list of annotations to evaluate * @return a set of values from a list of annotations */ private AnnotationValues annotationValues(List<Annotation> annotations) { AnnotationValues av = new AnnotationValues(); for (Annotation annotation : annotations) { //if there is a value, use it, but don't overwrite existing with empty Optional<I18NKey> optKey = retrieveKey(annotation, "caption"); if (optKey.isPresent()) { av.captionKey = optKey; } optKey = retrieveKey(annotation, "description"); if (optKey.isPresent()) { av.descriptionKey = optKey; } optKey = retrieveKey(annotation, "value"); if (optKey.isPresent()) { av.valueKey = optKey; } Optional<Locale> optLocale = retrieveLocale(annotation); if (optLocale.isPresent()) { av.locale = optLocale; } } return av; } /** * Returns an I18NKey value for the {@code annotationMethod} or Optional.empty() if none is found (which could be * either the method not being present or present but not returning a value. * * @param i18NAnnotation the annotation to assess * @param annotationMethod the method name to look for * @return an I18NKey value for the {@code annotationMethod} or Optional.empty() if none is found */ protected Optional<I18NKey> retrieveKey(Annotation i18NAnnotation, String annotationMethod) { checkNotNull(i18NAnnotation); checkNotNull(annotationMethod); Method[] methods = i18NAnnotation.annotationType().getDeclaredMethods(); for (Method method : methods) { if (method.getName().equals(annotationMethod)) { try { Object result = method.invoke(i18NAnnotation); if (result != null) { I18NKey key = (I18NKey) result; return Optional.of(key); } else { return Optional.empty(); } } catch (Exception e) { log.error("Unable to read annotation", e); } } } return Optional.empty(); } /** * returns a locale from {@code i18NAnnotation} if it has one, or Optional.empty() if it has not * * @param i18NAnnotation the annotation to assess * @return a locale from {@code i18NAnnotation} if it has one, or Optional.empty() if it has not */ protected Optional<Locale> retrieveLocale(Annotation i18NAnnotation) { checkNotNull(i18NAnnotation); //if there is not locale method, simply return empty() try { Method method = i18NAnnotation.annotationType().getDeclaredMethod("locale"); String tag = (String) method.invoke(i18NAnnotation); if ((tag == null) || (tag.isEmpty())) { return Optional.empty(); } return Optional.of(Locale.forLanguageTag(tag)); } catch (NoSuchMethodException e) { return Optional.empty(); } catch (Exception e) { log.error("Unable to read annotation", e); return Optional.empty(); } } /** * Applies annotation values to {@code component} * * @param component the component to be updated * @param annotationValues the annotation values to apply * @param annotationInfo used primarily to identify the Field, and therefore its name */ private void applyAnnotationValues(AbstractComponent component, AnnotationValues annotationValues, AnnotationInfo annotationInfo) { // set locale first Locale locale = annotationValues.locale.isPresent() ? annotationValues.locale.get() : currentLocale.getLocale(); component.setLocale(locale); // set caption, description & value if available if (annotationValues.captionKey.isPresent()) { component.setCaption(translate.from(annotationValues.captionKey.get(), locale)); } if (annotationValues.descriptionKey.isPresent()) { component.setDescription(translate.from(annotationValues.descriptionKey.get(), locale)); } if (annotationValues.valueKey.isPresent()) { if (component instanceof HasValue) { ((HasValue) component).setValue(translate.from(annotationValues.valueKey.get(), locale)); return; } else { log.warn("Field {} has a value annotation but does not implement HasValue. Annotation ignored", annotationInfo.getField().getName()); } } } /** * Sets the I18N values for the Grid itself, and also iterates the columns for column ids which are I18NKeys, and translates those as well * * @param grid the Grid to process * @param annotationValues the annotation values to apply * @param annotationInfo used primarily to identify the Field, and therefore its name */ protected void processGrid(Grid grid, AnnotationValues annotationValues, AnnotationInfo annotationInfo) { // do the grid itself applyAnnotationValues(grid, annotationValues, annotationInfo); // now do the column headers Locale locale = annotationValues.locale.isPresent() ? annotationValues.locale.get() : currentLocale.getLocale(); final List<Grid.Column> columns = grid.getColumns(); I18NKeyConverter converter = new I18NKeyConverter(); for (Grid.Column column : columns) { try { I18NKey columnKey = converter.convertToModel(column.getId()); String header = translate.from(columnKey, locale); column.setCaption(header); } catch (Exception e) { log.debug("Column id {} is not an I18NKey", column.getId()); } } } private static class AnnotationValues { Optional<I18NKey> captionKey = Optional.empty(); Optional<I18NKey> descriptionKey = Optional.empty(); Optional<I18NKey> valueKey = Optional.empty(); Optional<Locale> locale = Optional.empty(); } private void readObject(ObjectInputStream inputStream) throws ClassNotFoundException, IOException { inputStream.defaultReadObject(); serializationSupport.deserialize(this); } }