11

There is a constructor with three parameters of type enum:

public SomeClass(EnumType1 enum1,EnumType2 enum2, EnumType3 enum3)
{...}

The three parameters of type enum are not allowd to be combined with all possible values:

Example:

EnumType1.VALUE_ONE, EnumType2.VALUE_SIX, EnumType3.VALUE_TWENTY is a valid combination.

But the following combination is not valid:

EnumType1.VALUE_TWO, EnumType2.VALUE_SIX, EnumType3.VALUE_FIFTEEN

Each of the EnumTypes knows with which values it is allowed to be combined:

EnumType1 and the two others implement a isAllowedWith() method to check that as follows:

public enum EnumType1 {

VALUE_ONE,VALUE_TWO,...;

    public boolean isAllowedWith(final EnumType2 type) {
    switch (this) {
        case VALUE_ONE:
            return type.equals(Type.VALUE_THREE);
        case VALUE_TWO:
            return true;
        case VALUE_THREE:
            return type.equals(Type.VALUE_EIGHT);
        ...
    }
}

I need to run that check at compile time because it is of extreme importance in my project that the combinations are ALWAYS correct at runtime.

I wonder if there is a possibility to run that check with user defined annotations?

Every idea is appreciated :)

starblue
  • 55,348
  • 14
  • 97
  • 151
  • 1
    This is definitely something that you can and should do with [apt](http://docs.oracle.com/javase/1.5.0/docs/guide/apt/GettingStarted.html), though I'm not familiar enough with it to write a qualified answer. – Andrzej Doyle Oct 16 '12 at 15:23

5 Answers5

4

The posts above don't bring a solution for compile-time check, here's mine:

Why not use concept of nested Enum.

You would have EnumType1 containing its own values + a nested EnumType2 and this one a nested EnumType3.

You could organize the whole with your useful combination. You could end up with 3 classes (EnumType1,2 and 3) and each one of each concerned value containing the others with the allowed associated values.

And your call would look like that (with assuming you want EnumType1.VALUE_ONE associated with EnumType2.VALUE_FIFTEEN) :

EnumType1.VALUE_ONE.VALUE_FIFTEEN  //second value corresponding to EnumType2

Thus, you could have also: EnumType3.VALUE_SIX.VALUE_ONE (where SIX is known by type3 and ONE by type1).

Your call would be change to something like:

public SomeClass(EnumType1 enumType)

=> sample:

SomeClass(EnumType1.VALUE_ONE.VALUE_SIX.VALUE_TWENTY) //being a valid combination as said

To better clarify it, check at this post: Using nested enum types in Java

Community
  • 1
  • 1
Mik378
  • 21,881
  • 15
  • 82
  • 180
  • But you can still do EnumType1.VALUE_TWO.VALUE_SIX.VALUE_FIFTEEN which is illegal in the OP's case – giorashc Oct 16 '12 at 15:42
  • No you can't. VALUE_FIFTEEN would not be contained in its nested enums. I just give the concept, values that I've chosen are for the example. – Mik378 Oct 16 '12 at 15:52
  • 1
    Ok got it. but still this is going to be a very cumbersome enum :) – giorashc Oct 16 '12 at 15:53
  • @giorashc Of course yes :) But unless a super-keywords list exists in Java, it is the first and the last solution I could bring. Effectively, it would be a sacred maze but it would work and as we said in French "Qui ne tente rien n'a rien" ;) – Mik378 Oct 16 '12 at 15:55
  • @Mik378 I think this is a pretty good idea. The problem is, that i cannot change the enum types (third party provided). Right now i think i have to initiate a redesign of this part of the application. – SixDoubleFiveTreeTwoOne Oct 16 '12 at 16:18
3

So the simplest way to do this is to 1) Define the documentation to explain valid combinations and
2) add the checks in the constructor

If a constructor throws an Exception than that is the responsibility of the invoker. Basically you would do something like this:

public MyClass(enum foo, enum bar, enum baz)  
{  
    if(!validateCombination(foo,bar,baz))
    {  
        throw new IllegalStateException("Contract violated");
    }  
} 


private boolean validateCombination(enum foo, enum bar, enum baz)  
{  
    //validation logic
} 

Now this part is absolutely critical. Mark the class a final, it is possible that a partially constructed object can be recovered and abused to break your application. With a class marked as final a malicious program cannot extend the partially constructed object and wreak havoc.

Woot4Moo
  • 23,987
  • 16
  • 94
  • 151
  • 2
    But this is still not a compile time check – giorashc Oct 16 '12 at 15:25
  • 1
    What about IllegalArgumentException instead of IllegalStateException? – Bhesh Gurung Oct 16 '12 at 15:27
  • 1
    @BheshGurung technically the program is in an illegal state, more so than it being an illegal argument. – Woot4Moo Oct 16 '12 at 15:28
  • 1
    @giorashc I understand this isn't a compile time check, but op said "Every idea is appreciated :)" – Woot4Moo Oct 16 '12 at 15:28
  • Yeah, but it seems he has the correct one for runtime (the approach that is) – giorashc Oct 16 '12 at 15:30
  • @Woot4Moo My current implementation uses a IllegalArgumentException, but when throwing, it is already too late to recover. Thus i really need that checked at compiletime. I guess I have to invest time into serious annotation processing research. Thank you anyway :) – SixDoubleFiveTreeTwoOne Oct 16 '12 at 15:37
  • 1
    @SixDoubleFiveTreeTwoOne so the question is why is it to late to recover at time of object instantiation? That implies to me there is some disjoint system hitting this just spewing forth garbage that you have to handle. Let me know because it may be the case this doesn't need to be handled at compile time. – Woot4Moo Oct 16 '12 at 15:41
  • well it is in fact bad design from the beginning that makes this necessary. I have no choice but to work with this as is :). The classes are also needed (in correct state) for reporting meaningful errors. So there is really no other way for me than to ensure correctness at compile time. – SixDoubleFiveTreeTwoOne Oct 16 '12 at 15:56
  • @SixDoubleFiveTreeTwoOne well you just said it yourself. This is bad design from the beginning. Do everything you can to get a meeting with the staff and redesign the system. Continuing down this path is insanity and a waste of time. If this has to be done at compile time what happens when you want to expose a service to someone? I would explain to management that the system is far to fragile to continue on and needs to be refreshed. – Woot4Moo Oct 16 '12 at 15:59
2

One alternative idea is to write some automated tests to catch this, and hook them into your build process as a compulsory step before packaging/deploying your app.

If you think about what you're trying to catch here, it's code which is legal but wrong. While you could catch that during the compilation phase, this is exactly what tests are meant for.

This would fit your requirement of not being able to build any code with an illegal combination, because the build would still fail. And arguably it would be easier for other developers to understand than writing your own annotation processor...

Andrzej Doyle
  • 102,507
  • 33
  • 189
  • 228
  • You are right in postulating proper testing...we already do that. Its just to unelegant to discover this type of logically wrong code that way. – SixDoubleFiveTreeTwoOne Oct 16 '12 at 16:17
  • I think you can make a case for it either way, since in your situation developers would be writing valid Java, which doesn't make sense/do the right thing within the context of your application. That's broadly what tests are meant to catch. (Though I do enjoy a good compiler check too, so I can't blame you for looking for alternatives.) – Andrzej Doyle Oct 17 '12 at 10:43
1

The only way I know is to work with annotations.

Here is what I do I mean. Now your constructor accepts 3 parameters:

public SomeClass(EnumType1 enum1,EnumType2 enum2, EnumType3 enum3){}

so you are calling it as following:

SomeClass obj = new SomeClass(EnumTupe1.VALUE1, EnumTupe2.VALUE2, EnumTupe1.VALUE3)

Change the constructor to be private. Create public constructor that accept 1 parameter of any type you want. It may be just a fake parameter.

public SomeClass(Placeholder p)

Now you have to require to call this constructor while each argument is annotated with special annotation. Let's call it TypeAnnotation:

SomeClass obj = new SomeClass(TypeAnnotation(
    type1=EnumType1.VALUE1, 
    type2=EnumTupe2.VALUE2,
    type3=EnumTupe1.VALUE3)
    p3);

The call is more verbose but this is what we have to pay for compile time validation.

Now, how to define the annotation?

@Documented @Retention({RetentionPolicy.RUNTIME, RetentionPolicy.SOURCE}) @Target(PARAMETER) @interface TypeAnnotation { EnumType1 type1(); EnumType2 type3(); EnumType3 type3(); }

Please pay attention that target is PARAMETER and retention values are RUNTIME and SOURCE.

RUNTIME allows reading this annotation at runtime, while SOURCE allows creating annotation processor that can validate the parameters at runtime.

Now the public constructor will call the 3-parameters private construcor:

public SomeClass(Placeholder p) { this(readAnnotation(EnumType1.class), readAnnotation(EnumType2.class), readAnnotation(EnumType3.class), ) }

I am not implementing readAnnotation() here: it should be static method that takes stack trace, goes 3 elements back (to caller of the public costructor) and parses annotation TypeAnnotation.

Now is the most interesting part. You have to implement annotation processor. Take a look here for instructions and here for an example of annotation processor.

You will have to add usage of this annotation processor to your build script and (optionally) to your IDE. In this case you will get real compilation error when your compatibility rules are violated.

I believe that this solution looks too complicated but if you really need this you can do this. It may take a day or so. Good luck.

AlexR
  • 114,158
  • 16
  • 130
  • 208
0

Well, I am not aware of a compile time check but I do not think it is possible because how can the compiler know which value will be passed to the constructor (In case the value of your enum variable is calculated in runtime (e.g. by an If clause) ? This can only be validated on runtime by using a validator method as you implemented for the enum types.

Example :

If in your code you have something like this :

EnumType1 enumVal;

if (<some condition>) {
  enumVal = EnumType2.VALUE_SIX;
} else {
  enumVal = EnumType2.VALUE_ONE;
}

There is no way the compiler can know which of the values will be assigned to enumVal so it won't be able to verify what is passed to the constructor until the if block is evaluated (which can be done only in runtime)

giorashc
  • 13,691
  • 3
  • 35
  • 71