I'm writing a function to check multiple conditions in an array, if they are all true then return true.
For example:
public class Attribute {
private final String key;
private final String value;
//...
}
boolean canContactDogOwner(List<Attribute> attributes) {
boolean hasDog = false;
boolean isSubscribed = false;
boolean isOkToCall = false;
for (var attribute : attributes) {
if (attribute.key().equals("dogName")) {
hasDog = true;
} else if (attribute.key().equals("isSubscribed") && attribute.value().equals("Y")) {
isSubscribed = true;
} else if (attribute.key().equals("okToCall") && attribute.value().equals("Y")) {
isOkToCall = true;
}
// 1.
}
return hasDog && isSubscribed && isOkToCall;
}
void foo() {
List<Attribute> attributes = new ArrayList<>();
attributes.add(new Attribute("isSubscribed", "Y"));
attributes.add(new Attribute("okToCall", "Y"));
attributes.add(new Attribute("mobile", "12345678"));
attributes.add(new Attribute("landline", "1346346"));
attributes.add(new Attribute("email", "white#email.com"));
attributes.add(new Attribute("dogName", "Alex"));
boolean canContact = canContactDogOwner(attributes);
}
Two questions:
When all conditions are meet, the loop can be break, but if I add a check there, we would be checking every step in the loop, which doesn't look good. Any suggestions?
Is there a better / concise way to do it?
Like following?
boolean canContactDogOwner(List<Attribute> attributes) {
return attributes.stream().allMatch(A,B,C);
}
You can modify method canContactDogOwner to be like this,
boolean canContactDogOwner(List<Attribute> attributes) {
List<Attribute> conditions = new ArrayList<>();
conditions.add(new Attribute("isSubscribed", "Y"));
conditions.add(new Attribute("okToCall", "Y"));
return attributes.containsAll(conditions) &&
attributes.stream().anyMatch((attribute -> attribute.key.equals("dogName")));
}
A working and cleaner approach (IMO) will be to use some abstract data type like Map in this case..
static boolean canContactDogOwner(List<Attribute> attributes){
Map<String, String> attributeMap = new HashMap<>(); // empty map
attributes.forEach(attr -> attributeMap.put(attr.getKey(), attr.getValue())); // populate map
return attributeMap.containsKey("dogName") &&
"Y".equals(attributeMap.get("isSubscribed")) &&
"Y".equals(attributeMap.get("okToCall")); // Constant-String-first on equals check to avoid nullPointerExc with less code, yet clean
}
The code above with the comment is self-explanatory, so not adding details of the code.
But it is worth mentioning that
the complexity is still O(n) like other solutions here, n - number of elements (attribute objects)
flexibility to add or remove more conditions in the return statement
map as a chosen data-type and <Constant>.equals check avoids key validation and nullPointerException respectively.
If you are fascinated with Java-Streams, you can modify the code like this too..
static boolean canContactDogOwner(List<Attribute> attributes){
Map<String, String> attributeMap = attributes.stream()
.collect(Collectors.toMap(Attribute::getKey, Attribute::getValue));
return attributeMap.containsKey("dogName") &&
"Y".equals(attributeMap.get("isSubscribed")) &&
"Y".equals(attributeMap.get("okToCall"));
}
You could check if all condition is meet only when you set a value to true,
it will happen only 3 time.
And more concise way, probably with stream().anyMatch() but i'm not sure it will be more readable
Stream and allMatch(Predicate predicate) is a better way to do it in my opinion, but keep in mind that allMatch() take a Predicate as an argument, so you need to provide one.
I would suggest you encapsulate the attributes and create a class
something like Owner.
public class Owner {
private boolean isSubscribed;
private boolean okToCall;
private String mobile;
private String landline;
private String email;
private Optional<String> dogName;
public Owner(boolean isSubscribed, boolean okToCall, String mobile, String landline, String email, Optional<String> dogName) {
this.isSubscribed = isSubscribed;
this.okToCall = okToCall;
this.mobile = mobile;
this.landline = landline;
this.email = email;
this.dogName = dogName;
}
public boolean canContact() {
return this.isSubscribed && this.okToCall;
}
public boolean hasDog() {
return dogName.isPresent();
}
}
This way you do not have to deal with the if loops, the Owner object will say if they have a dog and can be contacted, etc.
public static void main(String[] args) {
Owner owner = new Owner(true, true, "12345678", "1346346", "white#email.com", Optional.of("Alex"));
boolean canContact = owner.hasDog() && owner.canContact();
}
I think you can have two lists of your conditions and attributes and then check whether attributes contain all condition or not.
public static Boolean allConditionsExist(List<String> attributes, List<String> conditions) {
return attributes.containsAll(conditions);
}
To convert your conditions and attributes to a list you can do something like this.
List<String> conditions = Arrays.asList("dogName","isSubscribed", "okToCall"); // add all your conditions
and
List<String> attributeKeys = attributes.stream().map(Attribute::getKey).collect(Collectors.toList());
Then call
allConditionExist(attributeKeys, conditions);
Assuming that every attribute is present only once, you could write
boolean canContactDogOwner(List<Attribute> attributes) {
int matches = 0;
for (var attribute : attributes) {
if (attribute.key().equals("dogName")) ||
attribute.key().equals("isSubscribed") && attribute.value().equals("Y") ||
attribute.key().equals("okToCall") && attribute.value().equals("Y"))
{
matches++;
if (matches >= 3) {
return true;
}
}
}
return false;
}
For the stream way you could write a Collector, constructed with a list of Predicates and returning a boolean. Wouldn't be the fastest...
Something like:
public class AllMatch<T> implements Collector<T, Set<Predicate<T>>, Boolean>
{
private Set<Predicate<T>> filter;
public AllMatch(Predicate<T>... filter)
{
super();
this.filter = new HashSet(Arrays.asList(filter));
}
#Override
public Supplier<Set<Predicate<T>>> supplier()
{
return () -> new HashSet<>();
}
#Override
public BinaryOperator<Set<Predicate<T>>> combiner()
{
return this::combiner;
}
#Override
public Set<Characteristics> characteristics()
{
return Stream.of(Characteristics.UNORDERED).collect(Collectors.toCollection(HashSet::new));
}
public Set<Predicate<T>> combiner(Set<Predicate<T>> left, Set<Predicate<T>> right)
{
left.addAll(right);
return left;
}
public Set<Predicate<T>> accumulator(Set<Predicate<T>> acc, T t)
{
filter.stream().filter(f -> f.test(t)).forEach(f ->
{
acc.add(f);
});
return acc;
}
#Override
public Function<Set<Predicate<T>>, Boolean> finisher()
{
return (s) -> s.equals(filter);
}
#Override
public BiConsumer<Set<Predicate<T>>, T> accumulator()
{
return this::accumulator;
}
public static void main(String[] args) {
Integer[] numbers = {1,2,3,4,5,6,7,8};
System.out.println(Arrays.stream(numbers).collect(new AllMatch<Integer>((i)-> i.equals(5),(i)-> i.equals(6))));
System.out.println(Arrays.stream(numbers).collect(new AllMatch<Integer>((i)-> i.equals(5),(i)-> i.equals(9))));
}
}
Related
Let's assume I have a class Person
public class Person {
private final String name;
private final int age;
private boolean rejected;
private String rejectionComment;
public void reject(String comment) {
this.rejected = true;
this.rejectionComment = comment;
}
// constructor & getters are ommited
}
and my app is something like that
class App {
public static void main(String[] args) {
List<Person> persons = Arrays.asList(
new Person("John", 10),
new Person("Sarah", 20),
new Person("Daniel", 30)
)
persons.forEach(p -> {
rejectIfYoungerThan15(p);
rejectIfNameStartsWithD(p);
// other rejection functions
}
}
private static void rejectIfYoungerThan15(Person p) {
if (!p.isRejected() && p.getAge() < 15) {
p.reject("Too young")
}
}
private static void rejectIfNameStartsWithD(Person p) {
if (!p.isRejected() && p.getName().startsWith("D")) {
p.reject("Name starts with 'D'")
}
}
// other rejection functions
}
The thing is I don't like that I have to perform !p.isRejected() check in every rejection function. Moreover, it doesn't make sense to pass an already rejected person to next filters.
So my idea is to use a mechanism of Stream.filter and make something like
persons.stream().filter(this::rejectIfYoungerThan15).filter(this::rejectIfNameStartsWithD)...
And change signature for these methods to return true if a passed Person has not been rejected and false otherwise.
But it seems to me that it's a very bad idea to use filter with non-pure functions.
Do you have any ideas of how to make it in more elegant way?
When you change the check functions to only check the condition (i.e. not to call p.isRejected()) and return boolean, you already made the necessary steps to short-circuit:
private static boolean rejectIfYoungerThan15(Person p) {
if(p.getAge() < 15) {
p.reject("Too young");
return true;
}
return false;
}
private static boolean rejectIfNameStartsWithD(Person p) {
if(p.getName().startsWith("D")) {
p.reject("Name starts with 'D'");
return true;
}
return false;
}
usable as
persons.forEach(p -> {
if(rejectIfYoungerThan15(p)) return;
if(rejectIfNameStartsWithD(p)) return;
// other rejection functions
}
}
A Stream’s filter operation wouldn’t do anything other than checking the returned boolean value and bail out. But depending on the Stream’s actual terminal operation the short-circuiting could go even farther and end up in not checking all elements, so you should not bring in a Stream operation here.
Calling these methods from lambda is fine, however, for better readability, you can rename these methods to show what they are doing and return boolean, e.g.:
private boolean hasEligibleAge(Person p){..}
private boolean hasValidName(Person p){..}
Another approach would be to wrap these methods into another method (to reflect the business logic/flow), e.g.:
private boolean isEligible(Person p){
//check age
//check name
}
You should make Person immutable, and let the reject-methods return a new Person. That will allow you to chain map-calls. Something like this:
public class Person {
private final String name;
private final int age;
private final boolean rejected;
private final String rejectionComment;
public Person reject(String comment) {
return new Person(name, age, true, comment);
}
// ...
}
class App {
// ...
private static Person rejectIfYoungerThan15(Person p) {
if (!p.isRejected() && p.getAge() < 15) {
return p.reject("Too young");
}
return p;
}
}
Now you can do this:
persons.stream()
.map(App::rejectIfYoungerThan15)
.map(App::rejectIfNameStartsWithD)
.collect(Collectors.toList());
If you want to remove rejected persons, you can add a filter after the mapping:
.filter(person -> !person.isRejected())
EDIT:
If you need to short circuit the rejections, you could compose your rejection functions into a new function and make it stop after the first rejection. Something like this:
/* Remember that the stream is lazy, so it will only call new rejections
* while the person isn't rejected.
*/
public Function<Person, Person> shortCircuitReject(List<Function<Person, Person>> rejections) {
return person -> rejections.stream()
.map(rejection -> rejection.apply(person))
.filter(Person::isRejected)
.findFirst()
.orElse(person);
}
Now your stream can look like this:
List<Function<Person, Person>> rejections = Arrays.asList(
App::rejectIfYoungerThan15,
App::rejectIfNameStartsWithD);
List<Person> persons1 = persons.stream()
.map(shortCircuitReject(rejections))
.collect(Collectors.toList());
I have two classes as shown below. I need to use these two classes to extract few things.
public final class ProcessMetadata {
private final String clientId;
private final String deviceId;
// .. lot of other fields here
// getters here
}
public final class ProcMetadata {
private final String deviceId;
private final Schema schema;
// .. lot of other fields here
}
Now I have below code where I am iterating above two classes and extracting schema given a clientId.
public Optional<Schema> getSchema(final String clientId) {
for (ProcessMetadata metadata1 : processMetadataList) {
if (metadata1.getClientId().equalsIgnoreCase(clientId)) {
String deviceId = metadata1.getDeviceId();
for (ProcMetadata metadata2 : procMetadataList) {
if (metadata2.getDeviceId().equalsIgnoreCase(deviceId)) {
return Optional.of(metadata2.getSchema());
}
}
}
}
return Optional.absent();
}
Is there any better way of getting what I need by iterating those two above classes in couple of lines instead of what I have? I am using Java 7.
You're doing a quadratic* search operation, which is inneficient. You can do this operation in constant time by first creating (in linear time) a mapping from id->object for each list. This would look something like this:
// do this once, in the constructor or wherever you create these lists
// even better discard the lists and use the mappings everywhere
Map<String, ProcessMetadata> processMetadataByClientId = new HashMap<>();
for (ProcessMetadata process : processMetadataList) {
processMetadataByClientId.put(process.getClientId(), process);
}
Map<String, ProcMetadata> procMetadataByDeviceId = new HashMap<>();
for (ProcMetadata metadata2 : procMetadataList) {
procMetadataByDeviceId.put(proc.getDeviceId(), proc);
}
Then your lookup simply becomes:
public Optional<Schema> getSchema(String clientId) {
ProcessMetadata process = processMetadataByClientId.get(clientId);
if (process != null) {
ProcMetadata proc = procMetadataByDeviceId.get(process.getDeviceId());
if (proc != null) {
return Optional.of(proc.getSchema());
}
}
return Optional.absent();
}
In Java 8 you could write it like this:
public Optional<Schema> getSchema(String clientId) {
return Optional.fromNullable(processMetadataByClientId.get(clientId))
.map(p -> procMetadataByDeviceId.get(p.getDeviceId()))
.map(p -> p.getSchema());
}
* In practice your algorithm is linear assuming client IDs are unique, but it's still technically O(n^2) because you potentially touch every element of the proc list for every element of the process list. A slight tweak to your algorithm can guarentee linear time (again assuming unique IDs):
public Optional<Schema> getSchema(final String clientId) {
for (ProcessMetadata metadata1 : processMetadataList) {
if (metadata1.getClientId().equalsIgnoreCase(clientId)) {
String deviceId = metadata1.getDeviceId();
for (ProcMetadata metadata2 : procMetadataList) {
if (metadata2.getDeviceId().equalsIgnoreCase(deviceId)) {
return Optional.of(metadata2.getSchema());
}
}
// adding a break here ensures the search doesn't become quadratic
break;
}
}
return Optional.absent();
}
Though of course using maps ensures constant-time, which is far better.
I wondered what could be done with Guava, and accidentally wrote this hot mess.
import static com.google.common.collect.Iterables.tryFind
public Optional<Schema> getSchema(final String clientId) {
Optional<String> deviceId = findDeviceIdByClientId(clientId);
return deviceId.isPresent() ? findSchemaByDeviceId(deviceId.get()) : Optional.absent();
}
public Optional<String> findDeviceIdByClientId(String clientId) {
return tryFind(processMetadataList, new ClientIdPredicate(clientId))
.transform(new Function<ProcessMetadata, String>() {
String apply(ProcessMetadata processMetadata) {
return processMetadata.getDeviceId();
}
});
}
public Optional<Schema> findSchemaByDeviceId(String deviceId) {
return tryFind(procMetadataList, new DeviceIdPredicate(deviceId.get())
.transform(new Function<ProcMetadata, Schema>() {
Schema apply(ProcMetadata procMetadata) {
return processMetadata.getSchema();
}
});
}
class DeviceIdPredicate implements Predicate<ProcMetadata> {
private String deviceId;
public DeviceIdPredicate(String deviceId) {
this.deviceId = deviceId;
}
#Override
public boolean apply(ProcMetadata metadata2) {
return metadata2.getDeviceId().equalsIgnoreCase(deviceId)
}
}
class ClientIdPredicate implements Predicate<ProcessMetadata> {
private String clientId;
public ClientIdPredicate(String clientId) {
this.clientId = clientId;
}
#Override
public boolean apply(ProcessMetadata metadata1) {
return metadata1.getClientId().equalsIgnoreCase(clientId);
}
}
Sorry.
I have an Item object having 4 String fields and 3 boolean fields.
I have to construct this object based on the 3 boolean variables.
The target is whenever any one of the boolean variable is true we have to create the object having that/those boolean variable set.
If for any situation none of the boolean variables are true, we wont create the object.
I am using a COR to check whether any of the boolean fields will be set or not based on some business logic.
I was trying this with builder, but then I have to construct so many objects and later discard them when none of the boolean variables found true.
Can anyone have any better idea, to solve this kind of problem ?
Well thanks for the 2 delete flag for this question. Thank for the thoughts on this question as well.
I did something to achieve what I want. Which is quite flexible I believe. Only part if there is a dependency on If loop, but that is acceptable since Report class can have extra boolean so when that class is changed, it's builder should be touched to cater that change. Rest this is flexible which I wanted.
public class Report {
private String acftNo;
private Date plannedDate;
private String plannedStn;
private Integer mntncId;
private Set<String> capableStations;
private String routedStn;
private boolean isRoutedNEQPlannedStn; //Inconsistency type 1
private boolean isCapableAtPlannedStn; //Inconsistency type 2
private boolean isPlannedOrRoutedStationExists; //Inconsistency type 3/5
public Report(String acftNo, Integer mntncId) {
super();
this.acftNo = acftNo;
this.mntncId = mntncId;
}
public Report(String acftNo, Date plannedDate, String plannedStn,
Integer mntncId) {
super();
this.acftNo = acftNo;
this.plannedDate = plannedDate;
this.plannedStn = plannedStn;
this.mntncId = mntncId;
}
//setters and getters. Removed for space.
public static Report buildReport(Maintenance<?> task, Set<InconsistencyReport> enumSet) {
Report temp = new Report(task.getAssignment().getAircraftNumber(),task.getAssignment().getMntncScheduleDate(),
task.getAssignment().getStationCode(),task.getAssignment().getMntncId());
temp.setCapableStations(InconsistencyReport.getCapableStations(task));
for(InconsistencyReport ir : enumSet)
{
if(ir.compareTo(InconsistencyReport.ROUTED_STN_NEQ_PLANNED_STN)==0)
temp.setRoutedNEQPlannedStn(true);
if(ir.compareTo(InconsistencyReport.ITEM_NT_CAPABLE_AT_PLANNED_STN)==0)
temp.setCapableAtPlannedStn(true);
if(ir.compareTo(InconsistencyReport.NO_ROUTD_STN_ON_A_DATE)==0)
temp.setPlannedOrRoutedStationExists(true);
}
return temp;
}
}
calculateInconsitencyReport() method which will decide whether to create object or not.
public class InconsistencyReportChain {
public enum InconsistencyReport implements InconsistencyReportIface {
ROUTED_STN_NEQ_PLANNED_STN {
#Override
public boolean findInconsistency(Maintenance<?> task ) {
if(!validate(task))
return false;
//some logic
return true;
return false;
}
},
ITEM_NT_CAPABLE_AT_PLANNED_STN {
#Override
public boolean findInconsistency(Maintenance<?> task) {
if(!validate(task))
return false;
//some logic
return true;
return false;
}
},
NO_ROUTD_STN_ON_A_DATE {
#Override
public boolean findInconsistency(Maintenance<?> task) {
if(!validate(task))
return false;
//some logic
return true
return false;
}
};
#Override
public boolean validate(Maintenance<?> task) {
return !(null == task.getAssignment());
}
static Set<String> getCapableStations(Maintenance<?> task)
{
Set<String> capableStations = newHashSet();
if(task.getCapStationList() != null)
{
capableStations.addAll(Arrays.asList(task.getCapStationList().split(StringConstants.COMMA_SPLIT_REGEX)));
}
if(task.getCapStationClassList() != null)
{
Map<String, List<String>> stationClassMap = CacheManager.get(STN_CLASS.name());
List<String> stationClass = Arrays.asList(task.getCapStationClassList().split(StringConstants.COMMA_SPLIT_REGEX));
for(String stnClass : stationClass)
{
capableStations.addAll(stationClassMap.get(stnClass));
}
}
return capableStations;
}
}
public static Report calculateInconsitencyReport(Maintenance<?> task) {
Set<InconsistencyReport> enumSet = null;
for(InconsistencyReport iReport : InconsistencyReport.values())
{
if(iReport.findInconsistency(task))
{
if(null==enumSet)
enumSet = EnumSet.of(iReport);
else
enumSet.add(iReport);
}
}
if(null!= enumSet && enumSet.size() > 0)
return Report.buildReport(task,enumSet);
return null;
}
}
Helper Interface:
public interface InconsistencyReportIface {
public boolean findInconsistency(Maintenance<?> task );
public boolean validate(Maintenance<?> task );
}
Details of class logic is teared off because of security.
What is the problem? Just create your object when one of your booleans is true.
if(bool1 || bool2 || bool3) {
item = new Item(str1, str2, str3, str4, bool1, bool2, bool3);
}
From what I understand of your description:
a) you will have some bools that will determine wether you create a certain object or not.
b) you may have to include some more bools into the "check protocol"
c) you have to do this checking in a loop where
i/ you check for the bool variable
ii/ you check if the object had been created previously
I still don't quite get it yet, but.. that looks pretty straight forward to me. Let's say your bools are stored in a boolean array boolean[] bools and your strings in a string array String[] strings (which, btw, I don't know what they are used for). You are saying to check if every bool is true and then create an object based on that result.
boolean[] bools = new boolean[] { ... };
String[] strings = new String[] { ... };
boolean checks = false;
for(int i = 0; i<bools.length && !checks; i++)
checks = bools[i];
//so far we will have processed if any of the bools was false, which was your condition
if(checks)
Object object = new Object(); //create your desired object
I don't understand why you would need to check if the object has been constructed previously, though, so I didn't include it in my suggestion :P
I've an enum like this:
public enum ChartType
{
TABLE(0, false), BAR(1, false), COLUMN(3, false)
private int type;
private boolean stacked;
ChartType(int type, boolean stacked)
{
this.type = type;
this.stacked = stacked;
}
public int getType()
{
return type;
}
public boolean isStacked()
{
return this.stacked;
}
}
I get a charttype (int values like 0,1,3) from the request and want the matching input
Something along these lines. Not sure if syntax is 100 percent, but it demos the idea.
public ChartType getChartypeForValue(int value)
for(ChartType type : ChartType.values()){
if(type.getType() == value){
return type;
}
}
return null;
}
You can add map in your enum to store relationship between type and specific enum value. You can fill it once in static block after all values will be created like:
private static Map<Integer, ChartType> typeMap = new HashMap<>();
static{
for (ChartType chartType: values()){
typeMap.put(chartType.type, chartType);
}
}
then you can add method which will use this map to get value you want, or null if there isn't any
public static ChartType getByType(int type) {
return typeMap.get(type);
}
You can use it like
ChartType element = ChartType.getByType(1);
You should create a static method to retrieve a type by number. With just a few charts like this, it's simplest to do that by just running through all the options. For larger enums, you could create a map for quick lookup. The only thing to watch for here is that any static initializers aren't run until after the values have been created.
Simple approach:
public static ChartType fromType(int type) {
// Or for (ChartType chart : ChartType.getValues())
for (ChartType chart : EnumSet.allOf(ChartType.class)) {
if (chart.type == type) {
return chart;
}
}
return null; // Or throw an exception
}
If you use java 8, use stream() and filter()
int value =2;
Optional<ChartType> chartType = Arrays.asList(ChartType.values()).stream().
filter(c -> c.type == value).findFirst();
if(chartType.isPresent()){
ChartType chartType =chartType.get();
//
}
Define new method:
public ChartType valueOf(int id) {
switch(id) {
case 1:
return TABLE;
case 2:
return BAR;
case 3:
return COLUMN;
}
return null;
}
Example:
ChartType.valueOf(1) // TABLE
Consider this case.
You have a class which you cannot change or extend in any way.
public class Foo {
...
private Boolean bar;
...
}
You need to edit the fields of that class via BeanEditor, but the logic behind that class allows and uses the fact that Boolean can have, so to say, 3 states: null, true and false.
Tapestry will, however, give you a checkbox with only 2 options, true or false.
So, people online suggest that you convert your Boolean type property to BooleanExtendedEnum type property which could represent three way logic.
public enum BooleanExtendedEnum {
UNDEFINED(null),
TRUE(Boolean.TRUE),
FALSE(Boolean.FALSE);
private Boolean booleanValue;
private static Map<Boolean, BooleanExtendedEnum> booleanToExtendedMap = new HashMap<Boolean, BooleanExtendedEnum>();
static {
for (BooleanExtendedEnum be : BooleanExtendedEnum.values()) {
booleanToExtendedMap.put(be.booleanValue, be);
}
}
private BooleanExtendedEnum(Boolean booleanValue) {
this.booleanValue = booleanValue;
}
public Boolean getBooleanValue() {
return booleanValue;
}
public static BooleanExtendedEnum getBooleanExtendedValue(Boolean booleanInput) {
return booleanToExtendedMap.get(booleanInput);
}
}
Since you cannot change your Foo class, you'll need to create a coercer for Boolean <=> BooleanExtendedEnum.
Coercion<Boolean, BooleanExtendedEnum> threeWayBooleanToExtended = new Coercion<Boolean, BooleanExtendedEnum>() {
#Override
public BooleanExtendedEnum coerce(Boolean input) {
if (input == null) {
return BooleanExtendedEnum.UNDEFINED;
} else {
return BooleanExtendedEnum.getBooleanExtendedEnumValue(input);
}
}
};
Coercion<BooleanExtendedEnum, Boolean> threeWayExtendedToBoolean = new Coercion<BooleanExtendedEnum, Boolean>() {
#Override
public Boolean coerce(BooleanExtendedEnum input) {
if (input == null) {
return null;
} else {
return input.getBooleanValue();
}
}
};
configuration.add(new CoercionTuple<Boolean, BooleanExtendedEnum>(Boolean.class, BooleanExtendedEnum.class, threeWayBooleanToExtended));
configuration.add(new CoercionTuple<BooleanExtendedEnum, Boolean>(BooleanExtendedEnum.class, Boolean.class, threeWayExtendedToBoolean));
Let's assume you have done something as simple as this in your BeanEditor in your tml:
<p:bar>
<div class="t-beaneditor-row">
<label>Bar Value</label>
<t:select t:id="fooBar" t:value="foo.bar" t:model="booleanExtendedSelectModel" t:blankOption="NEVER"/>
</div>
</p:bar>
... and provided the SelectModel like this:
public SelectModel getBooleanExtendedSelectModel() {
return new EnumSelectModel(BooleanExtendedEnum.class, messages);
}
Tapestry will create a drop-down list with three options
Undefined
True
False
However, the real Boolean values it will coerce those displayed values to will be
Undefined -> true
True -> true
False -> false
How can one achieve the desired effect (Undefined -> null), with limitations of not changing the class or wrapping it in another class which has Boolean type fields replaced with BooleanExtendedEnum type ones or using any other "hacky" solution?
The "glue" between the BeanEditor and the backing bean is the BeanModel. BeanModels are created by the BeanModelSource which in turn uses PropertyConduitSource.
It's quite simple to decorate the PropertyConduitSource to use Ternary instead of Boolean.
eg
public class MyAppModule {
public PropertyConduitSource decoratePropertyConduitSource(final PropertyConduitSource old) {
return new PropertyConduitSource() {
public PropertyConduit create(Class rootType, String expression) {
PropertyConduit conduit = old.create(rootType, expression);
// you cound also check for conduit.getAnnotation(AllowNull.class)
// and then annotate your bean properties for a more granular approach
if (Boolean.class.equals(conduit.getPropertyType()) {
return new TernaryPropertyConduit(conduit);
}
return conduit;
}
}
}
}
public class TernaryPropertyConduit implements PropertyConduit {
private PropertyConduit delegate;
public getPropertyType() { return Ternary.class };
public set(Object instance, Object value) {
delegate.set(instance, ((Ternary) value).asBoolean());
}
public get(Object) {
Boolean bValue = (Boolean) delegate.get(instance);
return Ternary.valueOf(instance);
}
}
You could add a property to your page and use a custom block.
public enum Ternary {
TRUE(Boolean.TRUE), FALSE(Boolean.FALSE), UNDEFINED(null);
public static Ternary valueOf(Boolean value) { ... }
public Boolean asBoolean() { ... }
}
public class MyPage {
#Property
private Foo foo;
public Ternary getTernaryBar() {
return Ternary.valueOf(foo.getBar());
}
public void setTernaryBar(Ternary tBar) {
foo.setBar(tBar.asBoolean());
}
}
<t:beaneditor t:id="foo" exclude="bar" add="ternaryBar">
<p:ternaryBar>
<t:label for="ternaryBar"/>
<t:select t:id="ternaryBar" />
</p:ternaryBar>
</t:beaneditor>