Skip to content

Commit

Permalink
Merge pull request #401 from sialcasa/398_bugfix_composite_validation…
Browse files Browse the repository at this point in the history
…_status

Fix for #398: CompositeValidator now correctly handles identical Vali…
  • Loading branch information
manuel-mauky authored Jun 15, 2016
2 parents 67f3439 + 9dfd33f commit 3b278c7
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 94 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*******************************************************************************
* Copyright 2015 Alexander Casall, Manuel Mauky
*
* 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 de.saxsys.mvvmfx.utils.validation;

import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* This class is used as {@link ValidationStatus} for {@link CompositeValidator}.
*
* In contrast to the basic {@link ValidationStatus} this class not only tracks
* {@link ValidationMessage} alone but also keeps track of the {@link Validator}s that
* the messages belong to. This is needed to be able to remove only those messages for
* a specific validator.
*
* @author manuel.mauky
*/
class CompositeValidationStatus extends ValidationStatus {

/**
* This wrapper class is used to additionally store the information of the validator
* that the messages belong to.
*
* Instead of storing the validator instance itself we only store an {@link System#identityHashCode(Object)}
* because we don't need the validator itself but only a way to distinguish validator instances.
* Using an identity hashcode instead of the actual instance can minimize the possibility of memory leaks.
*/
private static class CompositeValidationMessageWrapper extends ValidationMessage {

private Integer validatorCode;

CompositeValidationMessageWrapper(ValidationMessage base, Validator validator) {
super(base.getSeverity(), base.getMessage());
this.validatorCode = System.identityHashCode(validator);
}

Integer getValidatorCode() {
return validatorCode;
}
}


void addMessage(Validator validator, List<? extends ValidationMessage> messages) {
/*
Instead of adding the messages directly to the message list ...
*/
getMessagesInternal().addAll(
messages.stream()
// ... we wrap them to keep track of the used validator.
.map(message -> new CompositeValidationMessageWrapper(message, validator))
.collect(Collectors.toList()));
}

/*
Remove all given messages for the given validator.
*/
void removeMessage(Validator validator, List<? extends ValidationMessage> messages) {
List<CompositeValidationMessageWrapper> messagesToRemove =
getMessagesInternal().stream()
.filter(messages::contains) // only the given messages
.filter(message -> (message instanceof CompositeValidationMessageWrapper))
.map(message -> (CompositeValidationMessageWrapper) message)
.filter(message -> message.getValidatorCode().equals(System.identityHashCode(validator)))
.collect(Collectors.toList());

getMessagesInternal().removeAll(messagesToRemove);
}

/*
* Remove all messages for this particular validator.
*/
void removeMessage(Validator validator) {
getMessagesInternal().removeIf(validationMessage -> {
if(validationMessage instanceof CompositeValidationMessageWrapper) {
CompositeValidationMessageWrapper wrapper = (CompositeValidationMessageWrapper) validationMessage;

return wrapper.getValidatorCode().equals(System.identityHashCode(validator));
}

return false;
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@
******************************************************************************/
package de.saxsys.mvvmfx.utils.validation;

import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

import java.util.HashMap;
import java.util.Map;

/**
* This {@link Validator} implementation is used to compose multiple other validators.
Expand All @@ -27,31 +33,75 @@
* @author manuel.mauky
*/
public class CompositeValidator implements Validator {

private CompositeValidationResult validationStatus = new CompositeValidationResult();


CompositeValidationStatus status = new CompositeValidationStatus();

private ListProperty<Validator> validators = new SimpleListProperty<>(FXCollections.observableArrayList());
private Map<Validator, ListChangeListener<ValidationMessage>> listenerMap = new HashMap<>();


public CompositeValidator() {

validators.addListener(new ListChangeListener<Validator>() {
@Override
public void onChanged(Change<? extends Validator> c) {
while(c.next()) {

// When validators are added...
c.getAddedSubList().forEach(validator -> {

ObservableList<ValidationMessage> messages = validator.getValidationStatus().getMessages();
// ... we first add all existing messages to our own validator messages.
status.addMessage(validator, messages);

final ListChangeListener<ValidationMessage> changeListener = change -> {
while(change.next()) {
// add/remove messages for this particular validator
status.addMessage(validator, change.getAddedSubList());
status.removeMessage(validator, change.getRemoved());
}
};

validator.getValidationStatus().getMessages().addListener(changeListener);

// keep a reference to the listener for a specific validator so we can later use
// this reference to remove the listener
listenerMap.put(validator, changeListener);
});


c.getRemoved().forEach(validator -> {
status.removeMessage(validator);

if(listenerMap.containsKey(validator)){
ListChangeListener<ValidationMessage> changeListener = listenerMap.get(validator);

validator.getValidationStatus().getMessages().removeListener(changeListener);
listenerMap.remove(validator);
}
});
}
}
});

}

public CompositeValidator(Validator... validators) {
this(); // before adding the validators we need to setup the listeners in the default constructor
addValidators(validators);
}


public void addValidators(Validator... validators) {
validationStatus.addResults(Stream.of(validators)
.map(Validator::getValidationStatus)
.collect(Collectors.toList()));
this.validators.addAll(validators);
}

public void removeValidators(Validator... validators) {
validationStatus.removeResults(Stream.of(validators)
.map(Validator::getValidationStatus)
.collect(Collectors.toList()));
this.validators.removeAll(validators);
}

@Override
public ValidationStatus getValidationStatus() {
return validationStatus;
return status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ public class ValidationStatus {
new FilteredList<>(messages, message -> message.getSeverity().equals(Severity.ERROR)));
private ObservableList<ValidationMessage> warningMessages = FXCollections.unmodifiableObservableList(
new FilteredList<>(messages, message -> message.getSeverity().equals(Severity.WARNING)));



ListProperty<ValidationMessage> getMessagesInternal() {
return messages;
}


void addMessage(ValidationMessage message) {
messages.add(message);
Expand All @@ -64,16 +69,26 @@ void removeMessage(Collection<? extends ValidationMessage> messages) {
void clearMessages() {
messages.clear();
}




/**
* @return an <strong>unmodifiable</strong> observable list of all messages.
*/
public ObservableList<ValidationMessage> getMessages() {
return unmodifiableMessages;
}



/**
* @return an <strong>unmodifiable</strong> observable list of all messages of severity {@link Severity#ERROR}.
*/
public ObservableList<ValidationMessage> getErrorMessages() {
return errorMessages;
}


/**
* @return an <strong>unmodifiable</strong> observable list of all messages of severity {@link Severity#WARNING}.
*/
public ObservableList<ValidationMessage> getWarningMessages() {
return warningMessages;
}
Expand Down
Loading

0 comments on commit 3b278c7

Please sign in to comment.