diff --git a/ganttzk/src/main/java/org/zkoss/ganttz/data/constraint/Constraint.java b/ganttzk/src/main/java/org/zkoss/ganttz/data/constraint/Constraint.java
new file mode 100644
index 000000000..35d873af0
--- /dev/null
+++ b/ganttzk/src/main/java/org/zkoss/ganttz/data/constraint/Constraint.java
@@ -0,0 +1,88 @@
+/*
+ * This file is part of ###PROJECT_NAME###
+ *
+ * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e
+ * Desenvolvemento Tecnolóxico de Galicia
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.zkoss.ganttz.data.constraint;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.zkoss.ganttz.util.WeakReferencedListeners;
+import org.zkoss.ganttz.util.WeakReferencedListeners.IListenerNotification;
+
+/**
+ * @author Óscar González Fernández
+ *
+ */
+public abstract class Constraint {
+
+ public interface IConstraintViolationListener {
+ public void constraintViolated(Constraint constraint, T value);
+ }
+
+ public static T apply(T currentValue, Constraint... constraints) {
+ return apply(currentValue, Arrays.asList(constraints));
+ }
+
+ public static T apply(T currentValue,
+ Collection> constraints) {
+ T result = currentValue;
+ for (Constraint each : constraints) {
+ result = each.applyTo(result);
+ }
+ for (Constraint each : constraints) {
+ if (!each.isSatisfiedBy(result)) {
+ each.fireNotSatisfied(result);
+ }
+ }
+ return result;
+ }
+
+ private WeakReferencedListeners> weakListeners = WeakReferencedListeners
+ .create();
+
+ public final T applyTo(T currentValue) {
+ T result = applyConstraintTo(currentValue);
+ if (!isSatisfiedBy(result)) {
+ throw new IllegalStateException(result
+ + " doesn't fulfill this constraint: " + this);
+ }
+ return result;
+ }
+
+ protected abstract T applyConstraintTo(T currentValue);
+
+ public abstract boolean isSatisfiedBy(T value);
+
+ private void fireNotSatisfied(final T value) {
+ weakListeners
+ .fireEvent(new IListenerNotification>() {
+
+ @Override
+ public void doNotify(
+ IConstraintViolationListener listener) {
+ listener.constraintViolated(Constraint.this, value);
+ }
+ });
+ }
+
+ public void addConstraintViolationListener(IConstraintViolationListener listener) {
+ weakListeners.addListener(listener);
+ }
+
+}
diff --git a/ganttzk/src/test/java/org/zkoss/ganttz/data/constraint/ConstraintTest.java b/ganttzk/src/test/java/org/zkoss/ganttz/data/constraint/ConstraintTest.java
new file mode 100644
index 000000000..b2b993ba1
--- /dev/null
+++ b/ganttzk/src/test/java/org/zkoss/ganttz/data/constraint/ConstraintTest.java
@@ -0,0 +1,137 @@
+/*
+ * This file is part of ###PROJECT_NAME###
+ *
+ * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e
+ * Desenvolvemento Tecnolóxico de Galicia
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.zkoss.ganttz.data.constraint;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.classextension.EasyMock.createNiceMock;
+import static org.easymock.classextension.EasyMock.replay;
+import static org.easymock.classextension.EasyMock.verify;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import org.zkoss.ganttz.data.constraint.Constraint.IConstraintViolationListener;
+
+/**
+ * @author Óscar González Fernández
+ *
+ */
+public class ConstraintTest {
+
+ private Constraint biggerThanFive = new Constraint() {
+
+ @Override
+ protected Integer applyConstraintTo(Integer currentValue) {
+ return currentValue;
+ }
+
+ @Override
+ public boolean isSatisfiedBy(Integer value) {
+ return value != null && value > 5;
+ }
+ };
+ private Constraint lessThanFive = new Constraint() {
+
+ @Override
+ public boolean isSatisfiedBy(Integer value) {
+ return value!=null && value <5;
+ }
+
+ @Override
+ protected Integer applyConstraintTo(Integer currentValue) {
+ return Math.min(4, currentValue);
+ }
+ };
+
+ @Test
+ public void ifThereIsNoConstraintsTheOriginalValueIsReturned() {
+ assertThat(Constraint.apply(2), equalTo(2));
+ }
+
+ @Test
+ public void aNewValueFullfillingTheConstraintIsReturned() {
+ Constraint biggerThanFive = new Constraint() {
+
+ @Override
+ protected Integer applyConstraintTo(Integer currentValue) {
+ return Math.max(6, currentValue);
+ }
+
+ @Override
+ public boolean isSatisfiedBy(Integer value) {
+ return value != null && value > 5;
+ }
+ };
+ assertThat(biggerThanFive.applyTo(5), equalTo(6));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void aConstraintMustReturnValuesThatSatisfiesItsCriterion() {
+ biggerThanFive.applyTo(4);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void applyingSeveralConstraintsCallsApplyToOfEveryone() {
+ List> constraints = new ArrayList>();
+ final int numerOfConstraints = 5;
+ for (int i = 0; i < numerOfConstraints; i++) {
+ Constraint constraint = createNiceMock(Constraint.class);
+ expect(constraint.applyConstraintTo(isA(Object.class)))
+ .andReturn(2).atLeastOnce();
+ expect(constraint.isSatisfiedBy(isA(Object.class))).andReturn(true)
+ .atLeastOnce();
+ constraints.add(constraint);
+ }
+ replay(constraints.toArray());
+ Constraint.apply(2, constraints);
+ verify(constraints.toArray());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void theLastConstraintHasPriority() {
+ Integer result = Constraint.apply(6, biggerThanFive,
+ lessThanFive);
+ assertThat(result, equalTo(4));
+ }
+
+ @Test
+ public void theViolatedConstraintsNotifiesItsListeners() {
+ final Constraint[] constraintViolated = new Constraint[1];
+ biggerThanFive
+ .addConstraintViolationListener(new IConstraintViolationListener() {
+
+ @Override
+ public void constraintViolated(
+ Constraint constraint, Integer value) {
+ constraintViolated[0] = constraint;
+ }
+ });
+ Constraint.apply(6, biggerThanFive, lessThanFive);
+ assertThat(constraintViolated[0], equalTo(biggerThanFive));
+ }
+
+}