Java tutorial
/* * Copyright (C) 2014 Bastien Aracil * * 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 net.femtoparsec.binding.property; import net.femtoparsec.binding.property.factory.ChainedBeanPropertyFactory; import net.femtoparsec.binding.property.factory.DefaultBeanPropertyFactory; import net.femtoparsec.binding.property.factory.Utils; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; import com.google.common.reflect.TypeToken; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.*; /** * Helper for creating the bean property list from a property path; * * @author Bastien Aracil * @version 21/11/2014 - 11:28:10 */ public class BeanPropertyHelper { private static BiMap<Class<?>, Class<?>> PRIMITIVE_TO_BOXED = HashBiMap.create(); static { PRIMITIVE_TO_BOXED.put(boolean.class, Boolean.class); PRIMITIVE_TO_BOXED.put(char.class, Character.class); PRIMITIVE_TO_BOXED.put(byte.class, Byte.class); PRIMITIVE_TO_BOXED.put(short.class, Short.class); PRIMITIVE_TO_BOXED.put(int.class, Integer.class); PRIMITIVE_TO_BOXED.put(long.class, Long.class); PRIMITIVE_TO_BOXED.put(float.class, Float.class); PRIMITIVE_TO_BOXED.put(double.class, Double.class); } /** * Soft cache for BeanProperty. */ private static final BeanPropertyCache BEAN_PROPERTY_CACHE = new BeanPropertyCache(); /** * Custom factory to add its own BeanProperty */ public static BeanPropertyFactory CUSTOM_BEAN_FACTORY = null; /** * The default bean property factory */ private static final BeanPropertyFactory DEFAULT_BEAN_PROPERTY_FACTORY = new DefaultBeanPropertyFactory(); /** * <p>Complete a chained list of bean properties with a extra path.</p> * * @param parentPath the parent chained list of properties * @param path the path to append to the chain * @return the new chained list of bean properties */ public static ImmutableList<BeanProperty> completeBeanPropertyList(ImmutableList<BeanProperty> parentPath, String path) { final List<String> tokens = Arrays.asList(path.split("\\.")); //get the type of the last token in the parent path and use it to construct the rest of the path final TypeToken<?> last = parentPath.get(parentPath.size() - 1).getPropertyType(); return buildBeanPropertyList(last, tokens, parentPath); } /** * <p>From a property path, create a list of property beans that can be used to query the value point by the property path.</p> * * <p>For instance, if you have :</p> * * <ul> * <li>a class <code>Address</code> with the properties : * <ol> * <li><code>city</code> (StringProperty)</li> * <li><code>street</code> (StringProperty)</li> * <li><code>streetNumber</code> (IntProperty)</li> * </ol> * </li> * <li>a class <code>Person</code> with the properties : * <ol> * <li><code>firstName</code> (StringProperty)</li> * <li><code>lastName</code> (StringProperty)</li> * <li><code>address</code> (ObjectProperty<Address>)</li> * </ol> * </li> * </ul> * * <p>You can call this method with <code>Person.class</code> and path="address.city" to get the chained list of bean property to get/set * the city of a person. It is preferable to use implementations of {@link net.femtoparsec.binding.PathProperty} directly.</p> * * @param clazz the class of the root element of the chain * @param path the path to the property * @return a list of bean properties */ public static ImmutableList<BeanProperty> buildBeanPropertyList(Class<?> clazz, String path) { final List<String> tokens = Arrays.asList(path.split("\\.")); return buildBeanPropertyList(TypeToken.of(clazz), tokens); } /** * <p>From a list of bean properties create a displayable version of the chain.</p> * @param beanProperties the bean property list * @return a representation of the chained of method used to get the property from the root */ public static String buildPath(Collection<BeanProperty> beanProperties) { final StringBuilder sb = new StringBuilder(); boolean first = true; for (BeanProperty beanProperty : beanProperties) { if (first) { sb.append("(").append(beanProperty.getBeanType().getType()).append(")"); first = false; } sb.append("."); sb.append(beanProperty.getGetterString()); } return sb.toString(); } /** * * @param resultTypeToken the type token to check * @param requestedResult the requested casting type * @return true requestedResult can be used for a {@link net.femtoparsec.binding.PathProperty} that has as <code>resultTypToken</code> as last type token in his property path. */ public static boolean isValidRequestedResult(TypeToken<?> resultTypeToken, Class<?> requestedResult) { //we need to check if the resultTypeToken can be cast (converted) to the requestResult final Class<?> resultRawType = resultTypeToken.getRawType(); if (requestedResult.isPrimitive()) { return requestedResult == resultRawType; } else if (resultTypeToken.isPrimitive()) { return isBoxer(resultRawType, requestedResult); } else { return requestedResult.isAssignableFrom(resultRawType); } } /** * Determine if a primitive type can be boxed to a given type * * @param primitiveToBox the primitive type to check * @param boxerType the boxer type to use for the check * @return true if the parameter boxerType can be used to box the given primitive type. */ private static boolean isBoxer(Class<?> primitiveToBox, Class<?> boxerType) { if (!primitiveToBox.isPrimitive() || boxerType.isPrimitive()) { throw new IllegalArgumentException( "'primitiveBox' and 'boxerType' parameters must be primitive and non primitive classes respectively"); } return boxerType.isAssignableFrom(PRIMITIVE_TO_BOXED.get(primitiveToBox)); } /** * <p>Create the {@link BeanPropertyFactory} used when creating the list of bean properties. The returned factory * will try to use the {@link #CUSTOM_BEAN_FACTORY} first if not null. The custom factory is called first and if it is not able to create the property * bean, the default factory will be used.</p> * * @return the bean property factory to create <code>BeanProperty</code> */ private static BeanPropertyFactory createFactory() { return CUSTOM_BEAN_FACTORY == null ? DEFAULT_BEAN_PROPERTY_FACTORY : new ChainedBeanPropertyFactory(CUSTOM_BEAN_FACTORY, DEFAULT_BEAN_PROPERTY_FACTORY); } private static ImmutableList<BeanProperty> buildBeanPropertyList(TypeToken<?> typeToken, Collection<String> tokens) { return buildBeanPropertyList(typeToken, tokens, ImmutableList.of()); } private static ImmutableList<BeanProperty> buildBeanPropertyList(TypeToken<?> typeToken, Collection<String> tokens, ImmutableList<BeanProperty> prefixBeanProperties) { final BeanPropertyFactory factory = createFactory(); final List<BeanProperty> building = new LinkedList<>(prefixBeanProperties); TypeToken<?> beanType = typeToken; for (String token : tokens) { final String propertyName = preparePropertyName(token); final Optional<BeanProperty> result; final BeanProperty cached = BEAN_PROPERTY_CACHE.getBeanProperty(typeToken, propertyName); if (cached != null) { result = Optional.of(cached); } else if (propertyName.startsWith(BeanProperty.INDEX_PREFIX)) { result = factory.createIndexedBeanProperty(beanType, propertyName.substring(1)); } else { result = factory.createBeanProperty(beanType, propertyName); } final BeanProperty beanProperty = result.orElseThrow(() -> new IllegalArgumentException( "Invalid property : '" + token + "'. Built path : " + buildPath(building))); if (cached == null) { BEAN_PROPERTY_CACHE.addBeanProperty(beanProperty); } beanType = beanProperty.getPropertyType(); building.add(beanProperty); } return ImmutableList.copyOf(building); } /** * <p>Prepare the property name. Currently, the method simply tests if the property * is an integer and if so add the "index prefix" to standardize notation. * * @param propertyName the property name to prepare * @return the prepared property */ private static String preparePropertyName(String propertyName) { if (null != Utils.parseInteger(propertyName)) { return BeanProperty.INDEX_PREFIX + propertyName; } return propertyName; } /** * * @author Bastien Aracil * @version 21/11/2014 - 14:17:09 */ public static class BeanPropertyCache { private final BiMap<Key, Reference<BeanProperty>> table = HashBiMap.create(200); private final ReferenceQueue<BeanProperty> queue = new ReferenceQueue<>(); private final Thread releaserThread; public BeanPropertyCache() { this.releaserThread = new Thread(new Runner(), "Bean Property Releaser"); this.releaserThread.setDaemon(true); this.releaserThread.start(); } public synchronized void addBeanProperty(BeanProperty beanProperty) { final Reference<BeanProperty> beanPropertyReference = new SoftReference<>(beanProperty, this.queue); table.put(new Key(beanProperty), beanPropertyReference); } public BeanProperty getBeanProperty(TypeToken<?> beanType, String propertyName) { final Key key = Key.with(beanType, propertyName); final Reference<BeanProperty> value = table.get(Key.with(beanType, propertyName)); final BeanProperty beanProperty = value == null ? null : value.get(); if (value != null && beanProperty == null) { table.remove(key); } return beanProperty; } private class Runner implements Runnable { @Override public void run() { try { Reference<? extends BeanProperty> reference = queue.remove(); table.inverse().remove(reference); } catch (InterruptedException e) { //quit the runner } } } private static class Key { private final TypeToken<?> beanType; private final String propertyName; public Key(TypeToken<?> beanType, String propertyName) { this.beanType = beanType; this.propertyName = propertyName; } public Key(BeanProperty beanProperty) { this(beanProperty.getBeanType(), beanProperty.getPropertyName()); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Key)) return false; Key key = (Key) o; return beanType.equals(key.beanType) && propertyName.equals(key.propertyName); } @Override public int hashCode() { int result = beanType.hashCode(); result = 31 * result + propertyName.hashCode(); return result; } public static Key with(TypeToken<?> beanType, String propertyName) { return new Key(beanType, propertyName); } } } }