/*
Copyright 2014 Red Hat, Inc. and/or its affiliates.
This file is part of darcy-ui.
This program 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.
This program 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, see <http://www.gnu.org/licenses/>.
*/
package com.redhat.darcy.ui.internal;
import static com.redhat.darcy.ui.matchers.DarcyMatchers.displayed;
import static com.redhat.darcy.ui.matchers.DarcyMatchers.present;
import static com.redhat.darcy.ui.matchers.RequiredListMatcher.hasCorrectNumberOfItemsMatching;
import static com.redhat.synq.HamcrestCondition.match;
import com.redhat.darcy.ui.DarcyException;
import com.redhat.darcy.ui.NoRequiredElementsException;
import com.redhat.darcy.ui.annotations.Context;
import com.redhat.darcy.ui.annotations.NotRequired;
import com.redhat.darcy.ui.annotations.Require;
import com.redhat.darcy.ui.annotations.RequireAll;
import com.redhat.darcy.ui.api.View;
import com.redhat.darcy.ui.api.elements.Element;
import com.redhat.darcy.ui.api.elements.Findable;
import com.redhat.darcy.ui.matchers.LoadConditionMatcher;
import com.redhat.synq.Condition;
import com.redhat.synq.HamcrestCondition;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class Analyzer {
private final Object view;
private final List<Field> required;
private List<RequiredList<Object>> requiredLists;
private List<Object> requiredObjects;
private List<Condition<?>> isLoaded;
private List<Condition<?>> isDisplayed;
private List<Condition<?>> isPresent;
/**
* @param view A view with at least one field that is an
* {@link com.redhat.darcy.ui.api.elements.Element}, {@link com.redhat.darcy.ui.api.View},
* {@link com.redhat.darcy.ui.api.elements.Findable}, or {@link java.util.List} of those types,
* and is annotated as required.
* @param fields All of the fields declared for the specified View (including fields in parent
* classes}. Fields are expected to be accessible.
*/
public Analyzer(Object view, List<Field> fields) {
this.view = Objects.requireNonNull(view, "view");
this.required = filterRequired(Objects.requireNonNull(fields, "fields"));
}
public List<Condition<?>> getLoadConditions() {
if (isLoaded == null) {
isLoaded = new ArrayList<>();
analyze();
isLoaded.addAll(requiredObjects.stream()
.map(o -> match(o, new LoadConditionMatcher()))
.collect(Collectors.toList()));
isLoaded.addAll(requiredLists.stream()
.map(l -> match(l.list(),
hasCorrectNumberOfItemsMatching(l.atLeast(), l.atMost(),
new LoadConditionMatcher())))
.collect(Collectors.toList()));
if(isLoaded.isEmpty()) {
throw new NoRequiredElementsException(view);
}
}
return isLoaded;
}
public List<Condition<?>> getDisplayConditions() {
if (isDisplayed == null) {
isDisplayed = new ArrayList<>();
analyze();
isDisplayed.addAll(requiredObjects.stream()
.filter(o -> o instanceof Element) // Should check instance or field type?
.map(e -> match((Element) e, displayed()))
.collect(Collectors.toList()));
isDisplayed.addAll(requiredLists.stream()
.filter(l -> Element.class.isAssignableFrom(l.genericType()))
.map(l -> match(l.list(),
hasCorrectNumberOfItemsMatching(l.atLeast(), l.atMost(), displayed())))
.collect(Collectors.toList()));
if(isDisplayed.isEmpty()) {
throw new NoRequiredElementsException(view);
}
}
return isDisplayed;
}
public List<Condition<?>> getIsPresentConditions() {
if (isPresent == null) {
isPresent = new ArrayList<>();
analyze();
isPresent.addAll(requiredObjects.stream()
.filter(o -> o instanceof Findable) // Should check instance or field type?
.map(f -> match((Findable) f, present()))
.collect(Collectors.toList()));
isPresent.addAll(requiredLists.stream()
.filter(l -> Findable.class.isAssignableFrom(l.genericType()))
.map(l -> match(l.list(),
hasCorrectNumberOfItemsMatching(l.atLeast(), l.atMost(), present())))
.collect(Collectors.toList()));
if(isPresent.isEmpty()) {
throw new NoRequiredElementsException(view);
}
}
return isPresent;
}
/**
* Reflectively examine the view, gathering, filtering, and sorting fields. The results are
* assigned to {@link #requiredLists} and {@link #requiredObjects}; fields that are lists and
* objects of fields that are not lists, respectively. This method is idempotent; subsequent
* calls after the first have no effect (fields need only be analyzed once).
*
* <p>Fields cannot be analyzed before they are assigned, which is why this analyze is delayed
* until needed. This way you can instantiate an Analyzer in a constructor or {@code <init>}
* without worrying about whether your class or subclass fields are assigned yet.
*/
private void analyze() {
if (requiredLists != null && requiredObjects != null) {
return;
}
requiredLists = required.stream()
.filter(this::isList)
.map(f -> new RequiredList<>(f, view))
.filter(l -> Element.class.isAssignableFrom(l.genericType()) ||
View.class.isAssignableFrom(l.genericType()) ||
Findable.class.isAssignableFrom(l.genericType()))
.collect(Collectors.toList());
requiredObjects = required.stream()
.filter(f -> !isList(f))
.map(this::fieldToObject)
.collect(Collectors.toList());
if (requiredLists.isEmpty() && requiredObjects.isEmpty()) {
throw new NoRequiredElementsException(view);
}
}
private List<Field> filterRequired(List<Field> fields) {
return fields.stream()
.filter(this::isViewElementFindableOrList)
.filter(this::isNotAnnotatedWithContext)
.filter(this::isRequired)
.collect(Collectors.toList());
}
private boolean isList(Field f) {
return List.class.isAssignableFrom(f.getType());
}
private Object fieldToObject(Field f) {
try {
return f.get(view);
} catch (IllegalAccessException e) {
throw new DarcyException("Couldn't analyze required fields.", e);
}
}
/**
* Those are only supported types which make sense to look at.
*/
private boolean isViewElementFindableOrList(Field field) {
Class<?> fieldType = field.getType();
return View.class.isAssignableFrom(fieldType)
|| Element.class.isAssignableFrom(fieldType)
|| Findable.class.isAssignableFrom(fieldType)
|| List.class.isAssignableFrom(fieldType);
}
/**
* Contexts must be implicitly present if anything in this view is is to be present.
*/
private boolean isNotAnnotatedWithContext(Field field) {
return field.getAnnotation(Context.class) == null;
}
/**
* Determines whether a field is required or not based on combination of Require, RequireAll,
* and NotRequired annotations.
*/
private boolean isRequired(Field field) {
return field.getAnnotation(Require.class) != null
// Use the field's declaring class for RequireAll; may be a super class
|| (field.getDeclaringClass().getAnnotation(RequireAll.class) != null
&& field.getAnnotation(NotRequired.class) == null);
}
}