org.trinity.foundation.api.render.binding.BinderImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.trinity.foundation.api.render.binding.BinderImpl.java

Source

/*******************************************************************************
 * Trinity Shell Copyright (C) 2011 Erik De Rijcke
 *
 * This file is part of Trinity Shell.
 *
 * Trinity Shell is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option) any
 * later version.
 *
 * Trinity Shell is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 ******************************************************************************/

package org.trinity.foundation.api.render.binding;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.util.concurrent.Futures.addCallback;
import static java.lang.String.format;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import javax.annotation.concurrent.ThreadSafe;

import org.apache.onami.autobind.annotations.Bind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trinity.foundation.api.render.binding.view.DataContext;
import org.trinity.foundation.api.render.binding.view.EventSignal;
import org.trinity.foundation.api.render.binding.view.EventSignalFilter;
import org.trinity.foundation.api.render.binding.view.EventSignals;
import org.trinity.foundation.api.render.binding.view.ObservableCollection;
import org.trinity.foundation.api.render.binding.view.PropertyAdapter;
import org.trinity.foundation.api.render.binding.view.PropertySlot;
import org.trinity.foundation.api.render.binding.view.PropertySlots;
import org.trinity.foundation.api.render.binding.view.delegate.ChildViewDelegate;
import org.trinity.foundation.api.render.binding.view.delegate.PropertySlotInvocatorDelegate;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;

import com.google.common.base.CaseFormat;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;

@Bind
@Singleton
@ThreadSafe
public class BinderImpl implements Binder {

    private static final Logger LOG = LoggerFactory.getLogger(BinderImpl.class);
    private static final Cache<Class<?>, Cache<String, Optional<Method>>> GETTER_CACHE = CacheBuilder.newBuilder()
            .build();
    private static final Cache<Class<?>, Field[]> DECLARED_FIELDS_CACHE = CacheBuilder.newBuilder().build();
    private static final String GET_BOOLEAN_PREFIX = "is";
    private static final String GET_PREFIX = "get";
    private final PropertySlotInvocatorDelegate propertySlotDelegate;
    private final Injector injector;
    private final ChildViewDelegate childViewDelegate;

    private final Map<Object, Object> dataContextValueByView = new WeakHashMap<>();
    private final Map<Object, Set<Object>> viewsByDataContextValue = new WeakHashMap<>();
    private final Map<Object, PropertySlots> propertySlotsByView = new WeakHashMap<>();
    private final Map<Object, ObservableCollection> observableCollectionByView = new WeakHashMap<>();
    private final Map<Object, EventSignals> inputSignalsByView = new WeakHashMap<>();
    private final Map<Object, Map<Object, DataContext>> dataContextByViewByParentDataContextValue = new WeakHashMap<>();

    @Inject
    BinderImpl(final Injector injector, final PropertySlotInvocatorDelegate propertySlotInvocatorDelegate,
            final ChildViewDelegate childViewDelegate) {
        this.injector = injector;
        this.childViewDelegate = childViewDelegate;
        this.propertySlotDelegate = propertySlotInvocatorDelegate;
    }

    @Override
    public ListenableFuture<Void> bind(final ListeningExecutorService modelExecutor, final Object model,
            final Object view) {
        checkNotNull(modelExecutor);
        checkNotNull(model);
        checkNotNull(view);

        return modelExecutor.submit(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                bindImpl(modelExecutor, model, view);
                return null;
            }
        });

    }

    protected void bindImpl(final ListeningExecutorService modelExecutor, final Object model, final Object view) {
        LOG.debug("Bind model={} to view={}", model, view);

        bindViewElement(modelExecutor, model, view, Optional.<DataContext>absent(), Optional.<EventSignals>absent(),
                Optional.<ObservableCollection>absent(), Optional.<PropertySlots>absent());
    }

    @Override
    public ListenableFuture<Void> updateBinding(final ListeningExecutorService modelExecutor, final Object model,
            final String propertyName) {
        checkNotNull(modelExecutor);
        checkNotNull(model);
        checkNotNull(propertyName);

        return modelExecutor.submit(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                updateBindingImpl(modelExecutor, model, propertyName);
                return null;
            }
        });
    }

    protected void updateBindingImpl(final ListeningExecutorService modelExecutor, final Object model,
            final String propertyName) {
        LOG.debug("Update binding for model={} of property={}", model, propertyName);

        updateDataContextBinding(modelExecutor, model, propertyName);
        updateProperties(model, propertyName);
    }

    protected void updateDataContextBinding(final ListeningExecutorService modelExecutor, final Object model,
            final String propertyName) {

        final Map<Object, DataContext> dataContextByView = this.dataContextByViewByParentDataContextValue
                .get(model);
        if (dataContextByView == null) {
            return;
        }
        for (final Entry<Object, DataContext> dataContextByViewEntry : dataContextByView.entrySet()) {

            final Object view = dataContextByViewEntry.getKey();
            final DataContext dataContext = dataContextByViewEntry.getValue();
            final Optional<DataContext> optionalDataContext = Optional.of(dataContext);
            final Optional<EventSignals> optionalInputSignals = Optional
                    .fromNullable(this.inputSignalsByView.get(view));
            final Optional<ObservableCollection> optionalObservableCollection = Optional
                    .fromNullable(this.observableCollectionByView.get(view));
            final Optional<PropertySlots> optionalPropertySlots = Optional
                    .fromNullable(this.propertySlotsByView.get(view));

            if (dataContext.value().startsWith(propertyName)) {

                bindViewElement(modelExecutor, model, view, optionalDataContext, optionalInputSignals,
                        optionalObservableCollection, optionalPropertySlots);
            }
        }
    }

    protected void updateProperties(final Object model, final String propertyName) {

        try {
            final Optional<Method> optionalGetter = findGetter(model.getClass(), propertyName);
            if (!optionalGetter.isPresent()) {
                return;
            }
            final Object propertyValue = optionalGetter.get().invoke(model);
            final Set<Object> views = this.viewsByDataContextValue.get(model);
            if (views == null) {
                return;
            }
            for (final Object view : views) {
                final PropertySlots propertySlots = this.propertySlotsByView.get(view);
                if (propertySlots == null) {
                    continue;
                }
                for (final PropertySlot propertySlot : propertySlots.value()) {
                    final String propertySlotPropertyName = propertySlot.propertyName();
                    if (propertySlotPropertyName.equals(propertyName)) {
                        invokePropertySlot(view, propertySlot, propertyValue);
                    }
                }
            }

        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | ExecutionException e) {
            // TODO explanation
            LOG.error("", e);
        }
    }

    protected void bindViewElement(final ListeningExecutorService modelExecutor, final Object inheritedDataContext,
            final Object view, final Optional<DataContext> optionalFieldLevelDataContext,
            final Optional<EventSignals> optionalFieldLevelEventSignals,
            final Optional<ObservableCollection> optionalFieldLevelObservableCollection,
            final Optional<PropertySlots> optionalFieldLevelPropertySlots) {
        checkNotNull(inheritedDataContext);
        checkNotNull(view);

        final Class<?> viewClass = view.getClass();

        // check for class level annotations if field level annotations are
        // absent
        final Optional<DataContext> optionalDataContext = optionalFieldLevelDataContext
                .or(Optional.<DataContext>fromNullable(viewClass.getAnnotation(DataContext.class)));
        final Optional<EventSignals> optionalEventSignals = optionalFieldLevelEventSignals
                .or(Optional.<EventSignals>fromNullable(viewClass.getAnnotation(EventSignals.class)));
        final Optional<ObservableCollection> optionalObservableCollection = optionalFieldLevelObservableCollection
                .or(Optional
                        .<ObservableCollection>fromNullable(viewClass.getAnnotation(ObservableCollection.class)));
        final Optional<PropertySlots> optionalPropertySlots = optionalFieldLevelPropertySlots
                .or(Optional.<PropertySlots>fromNullable(viewClass.getAnnotation(PropertySlots.class)));

        Object dataContext = inheritedDataContext;
        if (optionalDataContext.isPresent()) {
            final Optional<Object> optionalDataContextValue = getDataContextValueForView(dataContext, view,
                    optionalDataContext.get());
            if (optionalDataContextValue.isPresent()) {
                dataContext = optionalDataContextValue.get();
            } else {
                //no data context value available so we're not going to bind the view.
                return;
            }
        }

        //TODO only register a view with a datacontext if they have a binding.
        //FIXME do a proper clean up of views with child datacontexes.
        //        if (optionalEventSignals.isPresent() || optionalObservableCollection.isPresent() || optionalPropertySlots.isPresent()) {
        //            registerBinding(dataContext,
        //                    view);
        //        }

        registerBinding(dataContext, view);

        if (optionalEventSignals.isPresent()) {
            final EventSignal[] eventSignals = optionalEventSignals.get().value();
            bindEventSignals(modelExecutor, dataContext, view, eventSignals);
        }

        if (optionalObservableCollection.isPresent()) {
            final ObservableCollection observableCollection = optionalObservableCollection.get();
            bindObservableCollection(modelExecutor, dataContext, view, observableCollection);
        }

        if (optionalPropertySlots.isPresent()) {
            final PropertySlots propertySlots = optionalPropertySlots.get();
            bindPropertySlots(dataContext, view, propertySlots);
        }

        bindChildViewElements(modelExecutor, dataContext, view);
    }

    protected void bindObservableCollection(final ListeningExecutorService modelExecutor, final Object dataContext,
            final Object view, final ObservableCollection observableCollection) {
        checkNotNull(dataContext);
        checkNotNull(view);
        checkNotNull(observableCollection);

        try {
            final String collectionProperty = observableCollection.value();

            final Optional<Method> collectionGetter = findGetter(dataContext.getClass(), collectionProperty);
            if (!collectionGetter.isPresent()) {
                return;
            }

            final Object collection = collectionGetter.get().invoke(dataContext);

            checkArgument(collection instanceof EventList, format(
                    "Observable collection must be bound to a property of type %s @ dataContext: %s, view: %s, observable collection: %s",
                    EventList.class.getName(), dataContext, view, observableCollection));

            final EventList<?> contextCollection = (EventList<?>) collection;
            final Class<?> childViewClass = observableCollection.view();

            try {
                contextCollection.getReadWriteLock().readLock().lock();

                for (int i = 0; i < contextCollection.size(); i++) {
                    final Object childViewDataContext = contextCollection.get(i);
                    final ListenableFuture<?> futureChildView = this.childViewDelegate.newView(view, childViewClass,
                            i);
                    addCallback(futureChildView, new FutureCallback<Object>() {
                        @Override
                        public void onSuccess(final Object childView) {
                            bindImpl(modelExecutor, childViewDataContext, childView);

                        }

                        @Override
                        public void onFailure(final Throwable t) {
                            LOG.error("Error while creating new child view.", t);
                        }
                    }, modelExecutor);
                }

                contextCollection.addListEventListener(new ListEventListener<Object>() {

                    // We use a shadow list because glazedlists does not
                    // give us the deleted object...
                    private final List<Object> shadowChildDataContextList = new ArrayList<Object>(
                            contextCollection);

                    @Override
                    public void listChanged(final ListEvent<Object> listChanges) {
                        handleListChanged(modelExecutor, view, childViewClass, this.shadowChildDataContextList,
                                listChanges);
                    }
                });

            } finally {
                contextCollection.getReadWriteLock().readLock().unlock();
            }

        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | ExecutionException e) {
            // TODO explanation
            LOG.error("", e);
        }
    }

    protected void handleListChanged(final ListeningExecutorService modelExecutor, final Object view,
            final Class<?> childViewClass, final List<Object> shadowChildDataContextList,
            final ListEvent<Object> listChanges) {
        while (listChanges.next()) {
            final int sourceIndex = listChanges.getIndex();
            final int changeType = listChanges.getType();
            final List<Object> changeList = listChanges.getSourceList();

            switch (changeType) {
            case ListEvent.DELETE: {
                final Object removedObject = shadowChildDataContextList.remove(sourceIndex);
                checkNotNull(removedObject);

                final Set<Object> removedChildViews = BinderImpl.this.viewsByDataContextValue.get(removedObject);
                for (final Object removedChildView : removedChildViews) {
                    BinderImpl.this.childViewDelegate.destroyView(view, removedChildView, sourceIndex);
                }

                break;
            }
            case ListEvent.INSERT: {
                final Object childViewDataContext = changeList.get(sourceIndex);
                checkNotNull(childViewDataContext);

                shadowChildDataContextList.add(sourceIndex, childViewDataContext);

                final ListenableFuture<?> futureChildView = this.childViewDelegate.newView(view, childViewClass,
                        sourceIndex);
                addCallback(futureChildView, new FutureCallback<Object>() {
                    @Override
                    public void onSuccess(final Object childView) {
                        bindImpl(modelExecutor, childViewDataContext, childView);

                    }

                    @Override
                    public void onFailure(final Throwable t) {
                        LOG.error("Error while creating new child view.", t);
                    }
                }, modelExecutor);
                break;
            }
            case ListEvent.UPDATE: {
                if (listChanges.isReordering()) {
                    final int[] reorderings = listChanges.getReorderMap();
                    for (int i = 0; i < reorderings.length; i++) {
                        final int newPosition = reorderings[i];
                        final Object childViewDataContext = changeList.get(sourceIndex);

                        shadowChildDataContextList.clear();
                        shadowChildDataContextList.add(newPosition, childViewDataContext);

                        final Set<Object> changedChildViews = BinderImpl.this.viewsByDataContextValue
                                .get(childViewDataContext);
                        for (final Object changedChildView : changedChildViews) {
                            BinderImpl.this.childViewDelegate.updateChildViewPosition(view, changedChildView, i,
                                    newPosition);
                        }
                    }
                } else {
                    final Object newChildViewDataContext = changeList.get(sourceIndex);
                    final Object oldChildViewDataContext = shadowChildDataContextList.set(sourceIndex,
                            newChildViewDataContext);
                    checkNotNull(oldChildViewDataContext);
                    checkNotNull(newChildViewDataContext);

                    final Object childView = BinderImpl.this.viewsByDataContextValue.get(oldChildViewDataContext);

                    bindImpl(modelExecutor, newChildViewDataContext, childView);
                }

                break;
            }
            }
        }
    }

    protected void bindEventSignals(final ListeningExecutorService modelExecutor, final Object dataContext,
            final Object view, final EventSignal[] eventSignals) {
        checkNotNull(dataContext);
        checkNotNull(view);
        checkNotNull(eventSignals);

        for (final EventSignal eventSignal : eventSignals) {
            final Class<? extends EventSignalFilter> eventSignalFilterType = eventSignal.filter();
            final String inputSlotName = eventSignal.name();

            // FIXME cache filter & uninstall any previous filter installments
            final EventSignalFilter eventSignalFilter = this.injector.getInstance(eventSignalFilterType);
            eventSignalFilter.installFilter(view,
                    new SignalImpl(modelExecutor, view, this.dataContextValueByView, inputSlotName));

        }
    }

    protected void registerBinding(final Object dataContext, final Object view) {
        checkNotNull(dataContext);
        checkNotNull(view);

        final Object oldDataContext = this.dataContextValueByView.put(view, dataContext);
        if (oldDataContext != null) {
            final Set<Object> oldDataContextViews = this.viewsByDataContextValue.get(oldDataContext);
            if (oldDataContextViews != null) {
                oldDataContextViews.remove(view);
            }
        }
        Set<Object> dataContextViews = this.viewsByDataContextValue.get(dataContext);
        if (dataContextViews == null) {
            dataContextViews = Sets.newSetFromMap(new WeakHashMap<Object, Boolean>());
            this.viewsByDataContextValue.put(dataContext, dataContextViews);
        }
        dataContextViews.add(view);
    }

    protected void bindPropertySlots(final Object dataContext, final Object view,
            final PropertySlots propertySlots) {
        checkNotNull(dataContext);
        checkNotNull(view);
        checkNotNull(propertySlots);

        this.propertySlotsByView.put(view, propertySlots);
        for (final PropertySlot propertySlot : propertySlots.value()) {
            bindPropertySlot(dataContext, view, propertySlot);
        }
    }

    protected void bindPropertySlot(final Object dataContext, final Object view, final PropertySlot propertySlot) {
        checkNotNull(dataContext);
        checkNotNull(view);
        checkNotNull(propertySlot);

        try {
            final String propertySlotDataContext = propertySlot.dataContext();
            final Object propertyDataContext;
            if (propertySlotDataContext.isEmpty()) {
                propertyDataContext = dataContext;
            } else {
                final Optional<Object> optionalRelativeDataContext = getDataContextValue(dataContext,
                        propertySlotDataContext);
                if (optionalRelativeDataContext.isPresent()) {
                    propertyDataContext = optionalRelativeDataContext.get();
                } else {
                    return;
                }
            }
            final String propertyName = propertySlot.propertyName();
            final Optional<Method> optionalGetter = findGetter(propertyDataContext.getClass(), propertyName);
            if (optionalGetter.isPresent()) {
                final Method getter = optionalGetter.get();

                // workaround for bug (4071957) submitted in
                // 1997(!) and still not fixed by sun/oracle.
                if (propertyDataContext.getClass().isAnonymousClass()) {
                    getter.setAccessible(true);
                }

                final Object propertyInstance = optionalGetter.get().invoke(propertyDataContext);

                invokePropertySlot(view, propertySlot, propertyInstance);

            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | ExecutionException e) {
            // TODO explanation
            LOG.error("", e);
        }
    }

    protected void invokePropertySlot(final Object view, final PropertySlot propertySlot,
            final Object propertyValue) {
        checkNotNull(view);
        checkNotNull(propertySlot);
        checkNotNull(propertyValue);

        try {
            final String viewMethodName = propertySlot.methodName();
            final Class<?>[] viewMethodArgumentTypes = propertySlot.argumentTypes();
            final Method targetViewMethod = view.getClass().getMethod(viewMethodName, viewMethodArgumentTypes);
            final Class<? extends PropertyAdapter<?>> propertyAdapterType = propertySlot.adapter();
            @SuppressWarnings("rawtypes")
            final PropertyAdapter propertyAdapter = propertyAdapterType.newInstance();
            @SuppressWarnings("unchecked")
            final Object argument = propertyAdapter.adapt(propertyValue);

            this.propertySlotDelegate.invoke(view, targetViewMethod, argument);
        } catch (final NoSuchMethodException | SecurityException | InstantiationException
                | IllegalAccessException e) {
            // TODO explanation
            LOG.error("", e);
        }
    }

    protected Optional<Object> getDataContextValueForView(final Object parentDataContextValue, final Object view,
            final DataContext dataContext) {
        checkNotNull(parentDataContextValue);
        checkNotNull(view);
        checkNotNull(dataContext);

        Map<Object, DataContext> dataContextByView = this.dataContextByViewByParentDataContextValue
                .get(parentDataContextValue);
        if (dataContextByView == null) {
            dataContextByView = new WeakHashMap<>();
            this.dataContextByViewByParentDataContextValue.put(parentDataContextValue, dataContextByView);
        }
        dataContextByView.put(view, dataContext);

        final String propertyChain = dataContext.value();
        return getDataContextValue(parentDataContextValue, propertyChain);
    }

    protected void bindChildViewElements(final ListeningExecutorService modelExecutor, final Object inheritedModel,
            final Object view) {
        checkNotNull(inheritedModel);
        checkNotNull(view);

        try {

            final Class<?> viewClass = view.getClass();

            final Field[] childViewElements = getDeclaredFields(viewClass);

            for (final Field childViewElement : childViewElements) {

                childViewElement.setAccessible(true);
                final Object childView = childViewElement.get(view);

                // filter out null values
                if (childView == null) {
                    continue;
                }

                // recursion safety
                if (this.dataContextValueByView.containsKey(childView)) {
                    continue;
                }

                final Optional<DataContext> optionalFieldDataContext = Optional
                        .<DataContext>fromNullable(childViewElement.getAnnotation(DataContext.class));
                final Optional<EventSignals> optionalFieldInputSignals = Optional
                        .<EventSignals>fromNullable(childViewElement.getAnnotation(EventSignals.class));
                final Optional<ObservableCollection> optionalFieldObservableCollection = Optional
                        .<ObservableCollection>fromNullable(
                                childViewElement.getAnnotation(ObservableCollection.class));
                final Optional<PropertySlots> optionalFieldPropertySlots = Optional
                        .<PropertySlots>fromNullable(childViewElement.getAnnotation(PropertySlots.class));

                bindViewElement(modelExecutor, inheritedModel, childView, optionalFieldDataContext,
                        optionalFieldInputSignals, optionalFieldObservableCollection, optionalFieldPropertySlots);

            }
        } catch (IllegalArgumentException | IllegalAccessException | ExecutionException e) {
            // TODO explanation
            LOG.error("", e);
        }
    }

    protected Iterable<String> toPropertyNames(final String subModelPath) {
        checkNotNull(subModelPath);

        return Splitter.on('.').trimResults().omitEmptyStrings().split(subModelPath);
    }

    protected Optional<Object> getDataContextValue(final Object model, final String propertyChain) {
        checkNotNull(model);
        checkNotNull(propertyChain);

        final Iterable<String> propertyNames = toPropertyNames(propertyChain);

        Object currentModel = model;
        try {

            for (final String propertyName : propertyNames) {
                if (currentModel == null) {
                    break;
                }
                final Class<?> currentModelClass = currentModel.getClass();
                final Optional<Method> foundMethod = findGetter(currentModelClass, propertyName);
                if (foundMethod.isPresent()) {
                    currentModel = foundMethod.get().invoke(currentModel);
                }
            }
        } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | ExecutionException e) {
            LOG.error(
                    String.format("Can not access getter on %s. Is it a no argument public method?", currentModel),
                    e);
        }
        return Optional.fromNullable(currentModel);
    }

    protected Optional<Method> findGetter(final Class<?> modelClass, final String propertyName)
            throws ExecutionException {
        checkNotNull(modelClass);
        checkNotNull(propertyName);
        return getGetterMethod(modelClass, propertyName);
    }

    protected Field[] getDeclaredFields(final Class<?> clazz) throws ExecutionException {
        return DECLARED_FIELDS_CACHE.get(clazz, new Callable<Field[]>() {
            @Override
            public Field[] call() {
                return clazz.getDeclaredFields();
            }
        });
    }

    protected Optional<Method> getGetterMethod(final Class<?> modelClass, final String propertyName)
            throws ExecutionException {
        return GETTER_CACHE.get(modelClass, new Callable<Cache<String, Optional<Method>>>() {
            @Override
            public Cache<String, Optional<Method>> call() {

                return CacheBuilder.newBuilder().build();
            }
        }).get(propertyName, new Callable<Optional<Method>>() {
            @Override
            public Optional<Method> call() {
                Method foundMethod = null;
                String getterMethodName = toGetterMethodName(propertyName);

                try {
                    foundMethod = modelClass.getMethod(getterMethodName);
                } catch (final NoSuchMethodException e) {
                    // no getter with get found,
                    // try with is.
                    getterMethodName = toBooleanGetterMethodName(propertyName);
                    try {
                        foundMethod = modelClass.getMethod(getterMethodName);
                    } catch (final NoSuchMethodException e1) {
                        // TODO explanation
                        LOG.error("", e1);

                    } catch (final SecurityException e1) {
                        LOG.error(format("Property %s is not accessible on %s. Did you declare it as public?",
                                propertyName, modelClass.getName()), e);
                    }
                } catch (final SecurityException e1) {
                    // TODO explanation
                    LOG.error(format("Property %s is not accessible on %s. Did you declare it as public?",
                            propertyName, modelClass.getName()), e1);
                }
                return Optional.fromNullable(foundMethod);
            }
        });
    }

    protected String toGetterMethodName(final String propertyName) {
        return toGetterMethodName(GET_PREFIX, propertyName);
    }

    protected String toGetterMethodName(final String prefix, final String propertyName) {
        checkNotNull(prefix);
        checkNotNull(propertyName);

        return prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, propertyName);
    }

    protected String toBooleanGetterMethodName(final String propertyName) {
        return toGetterMethodName(GET_BOOLEAN_PREFIX, propertyName);
    }
}