├── .gitignore ├── .travis.yml ├── immutator-dependencies.png ├── src ├── main │ └── java │ │ └── com │ │ └── javax0 │ │ └── immutator │ │ ├── ImmutableExtender.java │ │ ├── VoidMethodFilter.java │ │ ├── InterfaceMethodFilter.java │ │ ├── FluentMethodFilter.java │ │ ├── FluentImmutable.java │ │ ├── Immutable.java │ │ └── InterfaceImmutable.java └── test │ └── java │ └── com │ └── javax0 │ └── immutator │ └── ImmutableTest.java ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .project 3 | .classpath 4 | .settings 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: mvn install -DskipTests=true -Dgpg.skip=true 3 | -------------------------------------------------------------------------------- /immutator-dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verhas/immutator/HEAD/immutator-dependencies.png -------------------------------------------------------------------------------- /src/main/java/com/javax0/immutator/ImmutableExtender.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | 4 | public class ImmutableExtender { 5 | private static FluentImmutable fluentImmutable = new FluentImmutable(); 6 | 7 | public T fluent(T original) throws Exception { 8 | return fluentImmutable.of(original); 9 | } 10 | 11 | public InterfaceImmutable using(Class interFace) throws Exception { 12 | return InterfaceImmutable.getInstance(interFace); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/javax0/immutator/VoidMethodFilter.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import com.javax0.djcproxy.CallbackFilter; 6 | 7 | public class VoidMethodFilter implements CallbackFilter{ 8 | private static final VoidMethodFilter INSTANCE = new VoidMethodFilter(); 9 | 10 | public static VoidMethodFilter getInstance() { 11 | return INSTANCE; 12 | } 13 | @Override 14 | public boolean accept(Method method) { 15 | return method.getReturnType().toString().equals("void"); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/javax0/immutator/InterfaceMethodFilter.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.Set; 5 | 6 | import com.javax0.djcproxy.CallbackFilter; 7 | 8 | public class InterfaceMethodFilter implements CallbackFilter { 9 | 10 | private final Set methodNames; 11 | public InterfaceMethodFilter(Set methodNames) { 12 | this.methodNames = methodNames; 13 | } 14 | 15 | @Override 16 | public boolean accept(Method method) { 17 | return methodNames.contains(method.getName()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/javax0/immutator/FluentMethodFilter.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import com.javax0.djcproxy.CallbackFilter; 6 | 7 | public class FluentMethodFilter implements CallbackFilter { 8 | private static final FluentMethodFilter INSTANCE = new FluentMethodFilter(); 9 | 10 | public static FluentMethodFilter getInstance() { 11 | return INSTANCE; 12 | } 13 | 14 | @Override 15 | public boolean accept(Method method) { 16 | return method.getReturnType().toString().equals("void") 17 | || method.getDeclaringClass().isAssignableFrom( 18 | method.getReturnType()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/javax0/immutator/FluentImmutable.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | import com.javax0.djcproxy.ProxyFactory; 4 | import com.javax0.djcproxy.interceptors.RuntimeExceptionInterceptor; 5 | 6 | public class FluentImmutable { 7 | 8 | private static final ProxyFactory fluentFactory = new ProxyFactory<>(); 9 | static { 10 | fluentFactory.setCallbackFilter(FluentMethodFilter 11 | .getInstance()); 12 | } 13 | 14 | /** 15 | * Returns an immutable version of the {@code original} object, similar to 16 | * {@link #chain(Object)} but does not allow any method that returns any 17 | * subclass of {@code T}. 18 | *

19 | * This way this method creates an immutable object for any fluent API 20 | * implementation. 21 | * 22 | * @param original 23 | * @return 24 | * @throws Exception 25 | */ 26 | public T of(T original) throws Exception { 27 | @SuppressWarnings("unchecked") 28 | ProxyFactory factory = (ProxyFactory) fluentFactory; 29 | T proxy = factory.create(original, 30 | RuntimeExceptionInterceptor.getInstance()); 31 | return proxy; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/javax0/immutator/Immutable.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | import com.javax0.djcproxy.ProxyFactory; 4 | import com.javax0.djcproxy.interceptors.RuntimeExceptionInterceptor; 5 | 6 | public class Immutable { 7 | public static final ImmutableExtender of = new ImmutableExtender(); 8 | 9 | private static final ProxyFactory globalFactory = new ProxyFactory<>(); 10 | static { 11 | globalFactory.setCallbackFilter(VoidMethodFilter.getInstance()); 12 | } 13 | 14 | /** 15 | * Return an immutable proxy version of the {@code original} object, which 16 | * throws runtime exception when calling any method that does not return any 17 | * value ({@code void}). This method assumes that all methods that do not 18 | * return any value are mutators and any method that returns some value are 19 | * query methods without altering the state of the object. 20 | * 21 | * @param original 22 | * @return 23 | * @throws Exception 24 | */ 25 | public static T of(T original) throws Exception { 26 | @SuppressWarnings("unchecked") 27 | ProxyFactory factory = (ProxyFactory) globalFactory; 28 | T proxy = factory.create(original, 29 | RuntimeExceptionInterceptor.getInstance()); 30 | return proxy; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/javax0/immutator/InterfaceImmutable.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | import java.util.WeakHashMap; 7 | 8 | import com.javax0.djcproxy.ProxyFactory; 9 | import com.javax0.djcproxy.interceptors.RuntimeExceptionInterceptor; 10 | 11 | /** 12 | * Create a 13 | * 14 | * @author verhasp 15 | * 16 | */ 17 | public class InterfaceImmutable { 18 | 19 | private static final WeakHashMap, InterfaceImmutable> interfaceImmutableFactories = new WeakHashMap<>(); 20 | private final InterfaceMethodFilter filter; 21 | private final ProxyFactory factory; 22 | 23 | private InterfaceImmutable(Class interFace) { 24 | Set methodNames = new HashSet<>(); 25 | addDeclaredMethodsToSet(methodNames, interFace); 26 | addOwnAndInheritedMethodsToSet(methodNames, interFace); 27 | filter = new InterfaceMethodFilter(methodNames); 28 | factory = new ProxyFactory<>(); 29 | factory.setCallbackFilter(filter); 30 | } 31 | 32 | private void addDeclaredMethodsToSet(Set methodNames, 33 | Class interFace) { 34 | for (Method method : interFace.getDeclaredMethods()) { 35 | methodNames.add(method.getName()); 36 | } 37 | } 38 | 39 | private void addOwnAndInheritedMethodsToSet(Set methodNames, 40 | Class interFace) { 41 | for (Method method : interFace.getMethods()) { 42 | methodNames.add(method.getName()); 43 | } 44 | } 45 | 46 | static synchronized InterfaceImmutable getInstance(Class interFace) { 47 | final InterfaceImmutable instance; 48 | if (interfaceImmutableFactories.containsKey(interFace)) { 49 | instance = interfaceImmutableFactories.get(interFace); 50 | } else { 51 | instance = new InterfaceImmutable(interFace); 52 | interfaceImmutableFactories.put(interFace, instance); 53 | } 54 | return instance; 55 | } 56 | 57 | /** 58 | * Returns an immutable version of the {@code original} object, similar to 59 | * {@link #chain(Object)} but does not allow any method that returns any 60 | * subclass of {@code T}. 61 | *

62 | * This way this method creates an immutable object for any fluent API 63 | * implementation. 64 | * 65 | * @param original 66 | * @return 67 | * @throws Exception 68 | */ 69 | public T of(T original) throws Exception { 70 | @SuppressWarnings("unchecked") 71 | ProxyFactory tFactory = (ProxyFactory) factory; 72 | T proxy = tFactory.create(original, 73 | RuntimeExceptionInterceptor.getInstance()); 74 | return proxy; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.javax0 5 | immutator 6 | 1.0.2-SNAPSHOT 7 | jar 8 | 9 | immutator 10 | http://maven.apache.org 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | org.sonatype.oss 18 | oss-parent 19 | 7 20 | 21 | 22 | 23 | 24 | The Apache Software License, Version 2.0 25 | http://www.apache.org/licenses/LICENSE-2.0.txt 26 | repo 27 | A business-friendly OSS license 28 | 29 | 30 | 31 | scm:git:git@github.com:verhas/immutator.git 32 | https://github.com/verhas/immutator 33 | scm:git:git@github.com:verhas/immutator.git 34 | 35 | 36 | 37 | 38 | 39 | maven-compiler-plugin 40 | 3.0 41 | 42 | 1.7 43 | 1.7 44 | 45 | 46 | 47 | com.github.github 48 | site-maven-plugin 49 | 0.7 50 | 51 | Building site for ${project.version} 52 | github 53 | 54 | 55 | 56 | 57 | site 58 | 59 | site-deploy 60 | 61 | ${project.version} 62 | false 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-gpg-plugin 70 | 1.4 71 | 72 | 73 | sign-artifacts 74 | verify 75 | 76 | sign 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | gitHub 88 | gitHub 89 | git:git@github.com/verhas/immutator/immutator 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.slf4j 97 | slf4j-api 98 | 1.7.5 99 | 100 | 101 | ch.qos.logback 102 | logback-classic 103 | 1.0.13 104 | test 105 | 106 | 107 | junit 108 | junit 109 | 4.11 110 | test 111 | 112 | 113 | com.javax0 114 | djcproxy 115 | 2.0.3 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | immutator 2 | ========= 3 | 4 | Immutator is a Java library to create immutable version of an object run time. 5 | An immutable version of an object is a proxy object that works on the original object, transparently 6 | passing the call to the original object but throws exception whenever the code calls a method that may 7 | modify the state of the original object 8 | 9 | A method call that does not modify the state of the original object gets through: 10 | 11 | ``` 12 | caller -- queryMethod() --> proxyObject -- queryMethod() --> originalObject 13 | ``` 14 | 15 | A method call that may modify the state of the object raises exception 16 | 17 | ``` 18 | throw exception 19 | ^ 20 | caller -- mutatorMethod() --> proxyObject() --+ 21 | ``` 22 | 23 | The creation of the immutator proxy object is done during run time using the jscglib library. 24 | 25 | To create an immutable version of an object you should simply call 26 | 27 | ``` 28 | MyClass object = new MyClass(); 29 | MyClass query = Immutable.of(object); 30 | ``` 31 | 32 | The object `query` will throw `RuntimeException` for any call to a method of class `MyClass` that is `void`. This simplest use assumes that the methods in `MyClass` are developed following the single responsible principle, that is: each method does one thing. If a method returns a value, it does just that and does not do anything else, specifically does not alter the state of the object. 33 | 34 | Some design patterns, however, allow having non `void` methods to alter the state of the object although the single responsibility is still met. This is when your classes are designed to provide fluent interface. In that case the setters/mutators return an object of the same type as the method was called (or a sub class) allowing to write consecutive calls in the form: 35 | 36 | ``` 37 | fluent.methodCall(1).methodCall(2).methodCall(3) 38 | ``` 39 | 40 | To create immutable version of those objects you can 41 | 42 | ``` 43 | MyClass query = Immutable.of.fluent(object) 44 | ``` 45 | 46 | This will create an immutable object that will throw `RuntimeException` when a method is called that is `void` or has a return value compatible with `MyClass`, and thus can be part of the fluent api and as such is assumed to be a mutator. 47 | 48 | When you have a class that does not follow neither of the above design patterns you should use the general call: 49 | 50 | ``` 51 | interface Query { 52 | ... all methods that are not mutators ... 53 | } 54 | MyClass query = Immutable.using(Query.class).of(object); 55 | ``` 56 | 57 | The interface `Query` is used to define the names of the methods that are mutators. The call to create the immutable version will treat all methods that are listed in the interface `Query` as immutators and for any other method `RuntimeException` will be thrown. 58 | 59 | Notes 60 | ----- 61 | 62 | The methods defined in the `java.lang.Object` are never intercepted. 63 | 64 | The interface defining the query methods can be a class. Implementation details are not checked by the library. Also the return value and the argument types and the number of the arguments in this interface or class are irrelevant, only the names of the inherited and declared methods are checked. The class need not implement the interface. The interface is simply used like a String array to get the set of names of the methods. 65 | 66 | The factories that are creating the proxy classes in the library try to reuse the already created classes, thus they will not pollute the permgen. They will, however generate a new proxy class for each (class, interface) pairs. 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/test/java/com/javax0/immutator/ImmutableTest.java: -------------------------------------------------------------------------------- 1 | package com.javax0.immutator; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class ImmutableTest { 7 | 8 | public static class A { 9 | private int i; 10 | 11 | public int getI() { 12 | return i; 13 | } 14 | 15 | public void setI(int i) { 16 | this.i = i; 17 | } 18 | 19 | } 20 | 21 | @Test(expected = RuntimeException.class) 22 | public void given_AnObject_when_CreatingImmutableVersion_then_CallingMutatorThrowsException() 23 | throws Exception { 24 | A a = new A(); 25 | a.setI(10); 26 | A b = Immutable.of(a); 27 | b.setI(20); 28 | } 29 | 30 | @Test 31 | public void given_AnObject_when_CreatingImmutableVersion_then_CallingQueryReturnsOriginalRetval() 32 | throws Exception { 33 | A a = new A(); 34 | a.setI(10); 35 | A b = Immutable.of(a); 36 | Assert.assertEquals(10, b.getI()); 37 | } 38 | 39 | public static class Fluent { 40 | private int i; 41 | 42 | public int getI() { 43 | return i; 44 | } 45 | 46 | public SubFluent setI(int i) { 47 | SubFluent sf = new SubFluent(); 48 | sf.setI(i); 49 | return sf; 50 | } 51 | } 52 | 53 | public static class SubFluent extends Fluent { 54 | private int i; 55 | 56 | public int getI() { 57 | return i; 58 | } 59 | 60 | public SubFluent setI(int i) { 61 | this.i = i; 62 | return this; 63 | } 64 | } 65 | 66 | @Test(expected = RuntimeException.class) 67 | public void given_AFluentObject_when_CreatingImmutableVersion_then_CallingMutatorThrowsException() 68 | throws Exception { 69 | Fluent a = new Fluent(); 70 | a.setI(10); 71 | Fluent b = Immutable.of.fluent(a); 72 | b.setI(20); 73 | } 74 | 75 | @Test 76 | public void given_AFluentObject_when_CreatingImmutableVersion_then_CallingQueryReturnsOriginalRetval() 77 | throws Exception { 78 | Fluent a = new Fluent(); 79 | a = a.setI(10); 80 | Fluent b = Immutable.of.fluent(a); 81 | Assert.assertEquals(10, b.getI()); 82 | } 83 | 84 | @Test 85 | public void given_AFluentObject_when_CreatingImmutableVersion_then_CallingMutatorReturnsOriginalRetval() 86 | throws Exception { 87 | Fluent a = new Fluent(); 88 | a = a.setI(10); 89 | Fluent b = Immutable.of(a); 90 | Fluent c = b.setI(20); 91 | Assert.assertTrue(c == a); 92 | } 93 | 94 | @Test 95 | public void given_TwoSimpleObjectsOfTheSameType_when_CreatingImmutableVersions_then_NoDuplicatedProxyClassIsCreated() 96 | throws Exception { 97 | A a1 = new A(); 98 | A a2 = new A(); 99 | A aa = Immutable.of(a1); 100 | A ab = Immutable.of(a2); 101 | Assert.assertEquals(aa.getClass(), ab.getClass()); 102 | } 103 | 104 | @Test 105 | public void given_TwoFluentObjectsOfTheSameType_when_CreatingImmutableVersions_then_NoDuplicatedProxyClassIsCreated() 106 | throws Exception { 107 | Fluent a1 = new Fluent(); 108 | Fluent a2 = new Fluent(); 109 | Fluent aa = Immutable.of(a1); 110 | Fluent ab = Immutable.of(a2); 111 | Assert.assertEquals(aa.getClass(), ab.getClass()); 112 | } 113 | 114 | public static class MutableClass { 115 | public void mutator() { 116 | } 117 | 118 | public void immutator() { 119 | } 120 | } 121 | 122 | interface ImmutableMethods { 123 | void immutator(); 124 | }; 125 | 126 | @Test(expected = RuntimeException.class) 127 | public void given_AClassAndImmutableMethodsInterface_when_CreatingImmutableVersion_then_CallingQueryRunsFine() 128 | throws Exception { 129 | MutableClass testObject = new MutableClass(); 130 | MutableClass immutable = Immutable.of.using(ImmutableMethods.class).of( 131 | testObject); 132 | immutable.immutator(); 133 | } 134 | 135 | @Test 136 | public void given_AClassAndImmutableMethodsInterface_when_CreatingImmutableVersion_then_CallingMutatorThrowsException() 137 | throws Exception { 138 | MutableClass testObject = new MutableClass(); 139 | MutableClass immutable = Immutable.of.using(ImmutableMethods.class).of( 140 | testObject); 141 | immutable.mutator(); 142 | } 143 | 144 | @Test 145 | public void given_TwoObjectOfTheSameClass_when_CreatingImmutableVersionBasedOnTheSameInterface_then_CreatingImmutableVersionCreatesOneClassOnly() 146 | throws Exception { 147 | MutableClass testObject1 = new MutableClass(); 148 | MutableClass immutable1 = Immutable.of.using(ImmutableMethods.class) 149 | .of(testObject1); 150 | MutableClass testObject2 = new MutableClass(); 151 | MutableClass immutable2 = Immutable.of.using(ImmutableMethods.class) 152 | .of(testObject2); 153 | Assert.assertEquals(immutable1.getClass(), immutable2.getClass()); 154 | } 155 | 156 | public static class CloneableA extends A implements Cloneable { 157 | 158 | @Override 159 | public CloneableA clone() throws CloneNotSupportedException { 160 | return (CloneableA) super.clone(); 161 | } 162 | } 163 | 164 | @Test 165 | public void given_AnObject_when_CreatingImmutableVersion_then_CloneableWorks() 166 | throws Exception { 167 | A a = new CloneableA(); 168 | a.setI(10); 169 | A b = Immutable.of(a); 170 | Assert.assertEquals(10, b.getI()); 171 | } 172 | } 173 | --------------------------------------------------------------------------------