Conditionally Remove Java Methods at Compile-Time
Asked Answered
A

4

12

I am trying to achieve something similar to the C# preprocessor. I am aware that Java does NOT have the same preprocessor capabilities, and am aware that there are ways to achieve similar results using design patterns such as Factory. However, I am still interested in finding a solution to this question.

Currently, what I do is create a class that contains several static final boolean attributes, such as the following example:

public class Preprocessor
{
  public static final boolean FULLACCESS = false;
}

I then use this in the following manner:

public ClassName getClassName()
{
    if(Preprocessor.FULLACCESS)
    {
        return this;
    }
    else
    {
        return this.DeepCopy();
    }
}

So far so good, this solves my problem (the example above is trivial, but I do use this in other instances where it is helpful). My question is, would there be a way to place the conditional around an entire method, so that the method itself would be unavailable given the correct "Preprocessor" variables? For example, I would like to be able to make a specific constructor available only for packages that are given "Full Access", as follows:

public ClassName()
{
    // do things
}

if(FULLACCESS)
{
public ClassName(ClassName thing)
{
    // copy contents from thing to the object being created
}
}

Again, I am aware of the limitations (or design decisions) of Java as a language, and am aware that in most circumstances this is unnecessary. As a matter of fact, I have considered simply creating these "extra" methods and placing the entire code of them within a conditional, while throwing an Exception if the conditional is not active, but that is a very crude solution that does not seem helpful to my programmers when I make these libraries available to them.

Thank you very much in advance for any help.

Edit:

To complement the question, the reason why I am attempting to do this is that by using exceptions as a solution, the IDE would display methods as "available" when they are actually not. However, again, it might just be a case of my being ignorant of Java.

The reasons for my wanting to do this are primarily so that I may have more than one public interface available, say, one restrictive where control is tighter within the methods, and one more permissive where direct alteration of attributes is allowed. However, I do also want to be able to actively remove portions of code from the .class, for instance, in a Product Line development approach where certain variants are not available.

Edit2.:

Furthermore, it is important to note that I will be generating the documentation conditionally as well. Therefore, each compiled version of the packages would have its own documentation, containing only that which is actually available.

Alphaalphabet answered 27/3, 2017 at 17:20 Comment(7)
Good question (+1), but I still disagree with your ending remarks about throwing an exception being crude and unhelpful. I was going to post that as an answer before I was done reading through your question. Throwing a security exception is exactly what you should do. In fact, Java has built in security packages which it uses internally for this sort of thing; you could either try to leverage the security related functionality which already exists, or you could create your own specific security exceptions.Siclari
@Aaron this is debateable, but indeed security manager is the way to go probably (if it supports this - I have no idea)Wurth
If a user of your library wants to call a method, and if the user might not have permission to use that method, there should not be some black magic involved. It should simply be something of the form try { securityCheckedMethod(); } catch (AccessException ex) { /* you-don't-have-permission handler */ }Siclari
It looks to me like you might be wanting a more permissive interface for testing - which might explain the reluctance to use security exceptions in the production version to address this. That would be similar to why I almost went with a preprocessor approach once (see my answer). But it turned out that other tools (reflection, especially with groovy) made more sense for that need.Diamagnetism
Or, if some other method is preferred, perhaps @FledglingPidgeon could elaborate more on what exactly (s)he hopes to have API users do to handle a method they don't have access to, even if it is described in the same hypothetical way as the if(FULLACCESS) example in the question. That should be part of the question.Siclari
What Mark Adelsberger mentions is precisely why I am trying to achieve this. While the throwing of a security exception is viable, it would require the programmer to actually build and run the program to achieve this result (AFAIK, I might just be ignorant of Java). What I want, in this particular instance, is to create one restrictive public interface which controls modifications, and one permissive public interface that allows the programmer to directly alter attributes. There are other situations, however, where I simply do not want the code to be visible should the .class be "hacked".Alphaalphabet
To complement what I just said, in regards to "black magic", the idea is simply that having the IDE display methods as "available" when they are actually not, does not seem helpful. However, again, it might just be a case of my being ignorant of Java, in which case a solution for the "permissive interface problem" could be achieved with security exceptions, even if it does not permit me to actively remove portions of code from the .class, for instance, in a Product Line development approach where certain variants are not available.Alphaalphabet
S
5

This answer is based partially on the comments you have left on the question and on Mark's answer.

I would suggest that you do this using Java interfaces which expose just the API that you desire. When you need a less restrictive API contract, extend an interface or create a separate implementation of an existing interface to get what you need.

public interface A
{
    void f();
}

A above is your general API. Now you want to have some special extra methods to test A or to debug it or manipulate it or whatever...

public interface B extends A
{
    void specialAccess();
}

Also, Java now supports default method implementations for interfaces which might be useful to you depending on how you implement your API. They take the following form...

public interface A
{
    List getList();

    // this is still only an interface, but you have a default impl. here
    default void add(Object o)
    {
        getList().add(o);
    }
}

You can read more about default methods on Oracle's page about it here.

In your API, your general distribution of it could include A and omit B entirely, and omit any implementations that offer the special access; then you can include B and special implementations for the special access version of the API you mentioned. This would allow plain old Java objects, nothing different to the code other than an extra interface and maybe an extra implementation of it. The custom part would just be in your packaging of the library. If you want to hand someone a "non-special" low-access version, hand them a jar that does not include B and does not include any possible BImplementation, possibly by having a separate build script.

I use Netbeans for my Java work, and I like to let it use the default build scripts that it auto generates. So if I were doing this and I were doing it in Netbeans, I would probably create two projects, one for base API and one for special-access API, and I would make the special-access one dependent on the base project. That would leave me with two jars instead of one, but I would be fine with that; if two jars bothered me enough I would go through the extra step mentioned above of making a build script for the special access version.


Some examples straight from Java

Swing has examples of this kind of pattern. Notice that GUI components have a void paint(Graphics g). A Graphics gives you a certain set of functionality. Generally, that g is actually a Graphics2D, so you can treat it as such if you so desire.

void paint(Graphics g)
{
    Graphics2d g2d = Graphics2d.class.cast(g);
}

Another example is with Swing component models. If you use a JList or a JComboBox to display a list of objects in a GUI, you probably do not use the default model it comes with if you want to change that list over time. Instead, you create a new model with added functionality and inject it.

JList list = new JList();
DefaultListModel model = new DefaultListModel();
list.setModel(model);

Now your JList model has extra functionality that is not normally apparent, including the ability to add and remove items easily.

Not only is extra functionality added this way, but the original author of ListModel did not even need to know that this functionality could exist.

Siclari answered 27/3, 2017 at 18:18 Comment(1)
Thank you very much, this solves the issues I am having. It is slightly more time-consuming than C#'s solution, but the result is mostly the same so that it can be achieved with minor modifications to the project architecture. It is likely that a special build script will be required later on due to scaling issues, but for the time being this is perfect.Alphaalphabet
D
6

Well, you can make it happen. A word of caution, though...

I can only think of one time when I thought this kind of approach was the best way, and it turned out I was wrong. The case of changing a class's public interface especially looks like a red flag to me. Throwing an exception when the access level isn't high enough to invoke the method might be more code-friendly.

But anyway, when I thought I wanted a preprocessor, what I did was to write one. I created a custom annotation to place on conditionally-available methods, grabbed a Java parser and wrote a little program that used the parser to find and remove methods that have the annotation. Then add that (conditionally) to the build process.

Because it turned out to be useless to me, I discarded mine; and I've never seen anyone else do it and publish it; so as far as I know you'd have to roll your own.

Diamagnetism answered 27/3, 2017 at 17:30 Comment(7)
What did you envision happening to API users who attempted to use the method? They would have gotten an error about the missing method in cases where they did not have the permission. Or was your system not like the OPs, where the OP wants a security permission system? It seems odd for me to think of an API where the documentation states "This method does not even exist for you if you do not have permission to use it."Siclari
@Aaron - In context I'm not sure why you think this is important, but if it helps you see what the point was, the annotation name was @TestOnlyDiamagnetism
I would like to avoid actually creating a full pre-processor if possible, though I am willing to do so if necessary. In regards to Aaron's question, in my situation the documentation itself would be generated in accordance to these preprocessor arguments. Therefore, the documentation of a library would be specific to that particular compiled version of it.Alphaalphabet
I am referring to the accessibility of the methods, not the annotation name to access them. Maybe I should elaborate with an example: I'm a user of your API. I want to use f() which is in the API. I call it - f();. Looks good to me, the API end user, as you have an f() if your API. Depending on how you have your framework set up, I either get a compile error unknown symbol, or I get a runtime error because f() does not exist at runtime. Either way, I, the API user, am surprised because it is not what I expect from 99.9% of past experience. I post on SO: API contains f but I get errorsSiclari
@FledglingPidgeon Ah, I see. After reading your comments on the question and here on this answer, I see what you mean. I am going to add an answer too, as you might be overthinking it.Siclari
@Aaron - As you should have been able to infer from the name of the annotation, the point is that no user would ever see the method, so it wouldn't be in the documentation. Now I ask again: since my advice is against using this approach, why are you so hung up on understanding why I considered it at some point in the past?Diamagnetism
@MarkAdelsberger I'm not hung up on why you considered it; I've been in similar boats before. No issues on the overall why, only on the how for the benefit of this Q&A thread. What I was more concerned about is understanding the issue better, and now I do. Because of that, I ended up leaving an answer where otherwise I would not. I also left a +1 on yours because you gave a good answer: it was both correct and it cautioned the OP about pitfalls in the road ahead. You also ended up teasing more useful info out of the OP which I would give you +2 for if I could.Siclari
S
5

This answer is based partially on the comments you have left on the question and on Mark's answer.

I would suggest that you do this using Java interfaces which expose just the API that you desire. When you need a less restrictive API contract, extend an interface or create a separate implementation of an existing interface to get what you need.

public interface A
{
    void f();
}

A above is your general API. Now you want to have some special extra methods to test A or to debug it or manipulate it or whatever...

public interface B extends A
{
    void specialAccess();
}

Also, Java now supports default method implementations for interfaces which might be useful to you depending on how you implement your API. They take the following form...

public interface A
{
    List getList();

    // this is still only an interface, but you have a default impl. here
    default void add(Object o)
    {
        getList().add(o);
    }
}

You can read more about default methods on Oracle's page about it here.

In your API, your general distribution of it could include A and omit B entirely, and omit any implementations that offer the special access; then you can include B and special implementations for the special access version of the API you mentioned. This would allow plain old Java objects, nothing different to the code other than an extra interface and maybe an extra implementation of it. The custom part would just be in your packaging of the library. If you want to hand someone a "non-special" low-access version, hand them a jar that does not include B and does not include any possible BImplementation, possibly by having a separate build script.

I use Netbeans for my Java work, and I like to let it use the default build scripts that it auto generates. So if I were doing this and I were doing it in Netbeans, I would probably create two projects, one for base API and one for special-access API, and I would make the special-access one dependent on the base project. That would leave me with two jars instead of one, but I would be fine with that; if two jars bothered me enough I would go through the extra step mentioned above of making a build script for the special access version.


Some examples straight from Java

Swing has examples of this kind of pattern. Notice that GUI components have a void paint(Graphics g). A Graphics gives you a certain set of functionality. Generally, that g is actually a Graphics2D, so you can treat it as such if you so desire.

void paint(Graphics g)
{
    Graphics2d g2d = Graphics2d.class.cast(g);
}

Another example is with Swing component models. If you use a JList or a JComboBox to display a list of objects in a GUI, you probably do not use the default model it comes with if you want to change that list over time. Instead, you create a new model with added functionality and inject it.

JList list = new JList();
DefaultListModel model = new DefaultListModel();
list.setModel(model);

Now your JList model has extra functionality that is not normally apparent, including the ability to add and remove items easily.

Not only is extra functionality added this way, but the original author of ListModel did not even need to know that this functionality could exist.

Siclari answered 27/3, 2017 at 18:18 Comment(1)
Thank you very much, this solves the issues I am having. It is slightly more time-consuming than C#'s solution, but the result is mostly the same so that it can be achieved with minor modifications to the project architecture. It is likely that a special build script will be required later on due to scaling issues, but for the time being this is perfect.Alphaalphabet
B
4

the only way in Java to reach that is to use preprocessor, for instance PostgresJDBC team uses java comment preprocessor for such manipulations, here is example from their Driver.java

  //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
  @Override
  public java.util.logging.Logger getParentLogger() {
    return PARENT_LOGGER;
  }
  //#endif
Bohunk answered 16/8, 2017 at 14:31 Comment(1)
Thank you very much @Igor Maznitsa it is very useful library. You saved my time.Robtrobust
S
0

With Gradle you can manage your sources and I think that no preprocessor macros are no longer needed. Right now in src directory you have main/java with all sources but if you need specific methods in e.g. debug and release builds to do / or not specific things then create debug/java and release/java in src and put YourClass there. Note that by doing this you'll have to have YourClass in debug/java and release/java but not in main/java.

Scyphozoan answered 27/3, 2017 at 17:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.