Javassist Classpool.get() does not find class when using fully qualified name - javassist

I am attempting to get a simple Javassist example going. Consider the following code. Assume variable classPath points to a correct class folder that contains the required .class file.
When running, the first invocation of classPool.get() succeeds and the second fails. The spec of method ClassPool.get() requires a fully-qualified class name. Why is that?
package com.domain.jcp;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
public class Jcp {
public static void main(String[] args) throws NotFoundException {
ClassPool classPool = ClassPool.getDefault();
String classPath = "[CORRECT PATH TRUNK]\\target\\test-classes\\com\\domain\\jcp";
classPool.insertClassPath(classPath);
CtClass clazz1 = classPool.get("JcpTest");
CtClass clazz2 = classPool.get("com.domain.jcp.JcpTest");
}
}
The folder layout is a standard layout for a Maven project.

You simply need to use the correct classpath. This is wrong, because you are pointing into a package subdirectory rather than straight to the root:
String classPath = "[CORRECT PATH TRUNK]\\target\\test-classes\\com\\domain\\jcp";
This is correct:
String classPath = "[CORRECT PATH TRUNK]\\target\\test-classes";
Then you need to use the fully qualified package name, i.e. of these
CtClass clazz1 = classPool.get("JcpTest");
CtClass clazz2 = classPool.get("com.domain.jcp.JcpTest");
only the latter is correct, while the former is not.
Update: Here is what happens if you misunderstand what a Java classpath is:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
public class JavassistGetClass {
private static final String BASE_PATH = "...";
public static void main(String[] args) throws NotFoundException {
ClassPool classPool = ClassPool.getDefault();
// How to correctly use a classpath in Javassist
String classPath = BASE_PATH + "\\target\\classes";
classPool.insertClassPath(classPath);
CtClass ctClass = classPool.get("de.scrum_master.stackoverflow.q70786275.Application");
System.out.println("Correct package name: " + ctClass.getPackageName());
// How NOT to use a classpath in Javassist (or anywhere else)
String classPathIncorrect = BASE_PATH + "\\target\\classes\\de\\scrum_master\\stackoverflow\\q70786275";
classPool.insertClassPath(classPathIncorrect);
CtClass ctClassIncorrect = classPool.get("Application");
System.out.println("Incorrect package name: " + ctClassIncorrect.getPackageName());
}
}
Console log:
Correct package name: de.scrum_master.stackoverflow.q70786275
Incorrect package name: null

Related

Class pointcut that intercepts a method call with field access

I am writing some aspects to make my gradle pugin development a little cleaner. In gradle, there is an interface, like this
interface Plugin {
def apply(Project project);
}
Applied to a plugin
class MyPlugin implements Plugin<Project> {
def apply(Project project) {
do stuff
}
}
Now, i want to be able to annotate this class like this
#OnlyAllowedOnRoot
class MyPlugin implements Plugin<Project> {
def apply(Project project) {
do stuff
}
}
and have a pointcut that fires when the 'apply' method is fired, and pick up the parameter. because the logic for the pointcut would be
if (project.rootProject.name != project.name) {
throw new GradleScriptExeption("This plugin can only be applied to root")
}
how would I do this? This example is the foundation for about a dozen other pointcuts i would want to write, but i really don't know where to start. I know i can annotate the apply method directly, but im worried about readability, which is mostly the reason why im doing this to begin with. I can if i have to, but id rather not. And because of the lifecycle of Gradle, it must be checked when the 'apply' method is called, it can't be checked at the instantiation.
Here is a complete example, but in Java, not in Groovy. It should not make any difference if you use Groovy, though. BTW, I did not want to add the Gradle API as a dependency to my project, so I just replicated the relevant parts of its API with the right package names and signatures. Not being a Gradle user myself, I implemented the root project property hierarchically as a direct parent, not as an absolute root. If Gradle does it differently, just adjust the condition in the aspect throwing the exception back to your own sample code.
Gradle API:
package org.gradle.api;
public class Project {
private String name;
private Project rootProject;
public Project(String name, Project rootProject) {
this.name = name;
this.rootProject = rootProject;
}
public String getName() {
return name;
}
public Project getRootProject() {
return rootProject;
}
#Override
public String toString() {
return "Project(name = " + name + ", rootProject = " + rootProject + ")";
}
}
package org.gradle.api;
public interface Plugin<T> {
void apply(T target);
}
package org.gradle.api;
public class GradleScriptExeption extends RuntimeException {
private static final long serialVersionUID = 1L;
public GradleScriptExeption(String message, Throwable cause) {
super(message, cause);
}
}
Marker annotation + plugins:
package de.scrum_master.app;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
class NormalPlugin implements Plugin<Project> {
public void apply(Project project) {}
}
package de.scrum_master.app;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
#Retention(RUNTIME)
public #interface OnlyAllowedOnRoot {}
package de.scrum_master.app;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
#OnlyAllowedOnRoot
class RootPlugin implements Plugin<Project> {
public void apply(Project project) {}
}
Driver application:
package de.scrum_master.app;
import org.gradle.api.Project;
public class Application {
public static void main(String[] args) {
Project rootProject = new Project("root", null);
Project childProject = new Project("child", rootProject);
Project grandChildProject = new Project("grandchild", childProject);
NormalPlugin normalPlugin = new NormalPlugin();
normalPlugin.apply(rootProject);
normalPlugin.apply(childProject);
normalPlugin.apply(grandChildProject);
RootPlugin rootPlugin = new RootPlugin();
rootPlugin.apply(rootProject);
rootPlugin.apply(childProject);
rootPlugin.apply(grandChildProject);
}
}
Aspect:
package de.scrum_master.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.gradle.api.GradleScriptExeption;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
#Aspect
public class GradlePluginAspect {
#Pointcut("execution(void apply(*)) && target(plugin) && args(project)")
private static void pluginExecution(Plugin plugin, Project project) {}
#Before("pluginExecution(plugin, project) && #target(de.scrum_master.app.OnlyAllowedOnRoot)")
public void illegalRootPlugin(JoinPoint thisJoinPoint, Plugin plugin, Project project) {
if (project.getRootProject() != null)
throw new GradleScriptExeption("Cannot apply " + plugin.getClass().getSimpleName() + " to non-root project " + project, null);
}
#Before("pluginExecution(plugin, project)")
public void logPluginApply(JoinPoint thisJoinPoint, Plugin plugin, Project project) {
System.out.println("Applying " + plugin.getClass().getSimpleName() + " to " + project);
}
}
Console log:
Applying NormalPlugin to Project(name = root, rootProject = null)
Applying NormalPlugin to Project(name = child, rootProject = Project(name = root, rootProject = null))
Applying NormalPlugin to Project(name = grandchild, rootProject = Project(name = child, rootProject = Project(name = root, rootProject = null)))
Applying RootPlugin to Project(name = root, rootProject = null)
Exception in thread "main" org.gradle.api.GradleScriptExeption: Cannot apply RootPlugin to non-root project Project(name = child, rootProject = Project(name = root, rootProject = null))
at de.scrum_master.aspect.GradlePluginAspect.illegalRootPlugin(GradlePluginAspect.aj:19)
at de.scrum_master.app.RootPlugin.apply(RootPlugin.java:8)
at de.scrum_master.app.Application.main(Application.java:18)

Syntax error when adding Runnable in static initializer with javassist

I want to use javassist (version 3.19-GA) to generate bytecode of a static initializer of a class that starts a thread. For some reason I cannot understand javassist expects a ";" somewhere even though I believe the code I provide is syntactically correct. Does someone see more than I do? Here is the code. What is the problem?
ClassPool pool = ClassPool.getDefault();
final CtClass clazz = pool.get(somename);
clazz.makeClassInitializer().insertAfter(
"try{Runnable r=new Runnable () {public void run () { System.out.println (\"hello!!!!\"); }}; " +
"new Thread(r).start(); } catch(Exception e){}");
I'm getting the following exception:
javassist.CannotCompileException: [source error] ; is missing
at javassist.CtBehavior.insertAfter(CtBehavior.java:877)
at javassist.CtBehavior.insertAfter(CtBehavior.java:792)
at my.code(myclass.java:111)
Thanks for any hint.
Most probably javassist compiler does not support anonymous inner classes like new Runnable () {...}
You have to create new class, inherit it from Runnable, implement method run and create object of this class in your constructor.
package hello;
import javassist.*;
class Test{
}
class Main {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
final CtClass clazz = pool.get(Test.class.getCanonicalName());
CtClass runnable = pool.makeClass("my.custom.RunnableImpl");
runnable.addInterface(pool.get("java.lang.Runnable"));
CtMethod method = CtNewMethod.make("public void run() { System.out.println(\"hello!!!!\"); }", runnable);
runnable.addMethod(method);
// load class
runnable.toClass();
clazz.setName("newTest");
CtConstructor ctConstructor = clazz.makeClassInitializer();
ctConstructor.insertAfter("try{ new Thread( new my.custom.RunnableImpl() ).start(); } catch(Exception e){}");
Class aClass = clazz.toClass();
// call initializer
Class.forName(aClass.getCanonicalName());
Thread.sleep(1000);
}
}
From the official documentation of void insertAfter(String src):
Parameters:
src - the source code representing the inserted bytecode. It must be a
single statement or block.
In your src String parameter, you don't provide a single statement or a block.
A block is "{}".
Try insertBefore(String src) method with global enclosing "{}":
ClassPool pool = ClassPool.getDefault();
final CtClass clazz = pool.get(somename);
clazz.makeClassInitializer().insertBefore(
"{try{Runnable r = new Runnable () {public void run () { System.out.println (\"hello!!!!\"); }}; " +
"new Thread(r).start(); } catch(Exception e){}}");

Arquillian Graphene #Location placeholder

I'm learning Arquillian right now I wonder how to create page that has a placeholder inside the path. For example:
#Location("/posts/{id}")
public class BlogPostPage {
public String getContent() {
// ...
}
}
or
#Location("/posts/{name}")
#Location("/specific-page?requiredParam={value}")
I have looking for an answer on graphine and arquillian reference guides without success. I used library from other language that have support for page-objects, but it has build-in support for placeholders.
AFAIK there is nothing like this implemented in Graphene.
To be honest, I'm not sure how this should behave - how would you pass the values...?
Apart from that, I think that it could be also limited by Java annotation abilities https://stackoverflow.com/a/10636320/6835063
This is not possible currently in Graphene. I've created ARQGRA-500.
It's possible to extend Graphene to add dynamic parameters now. Here's how. (Arquillian 1.1.10.Final, Graphene 2.1.0.Final.)
Create an interface.
import java.util.Map;
public interface LocationParameterProvider {
Map<String, String> provideLocationParameters();
}
Create a custom LocationDecider to replace the corresponding Graphene's one. I replace the HTTP one. This Decider will add location parameters to the URI, if it sees that the test object implements our interface.
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Map.Entry;
import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.graphene.location.decider.HTTPLocationDecider;
import org.jboss.arquillian.graphene.spi.location.Scheme;
import org.jboss.arquillian.test.spi.context.TestContext;
public class HTTPParameterizedLocationDecider extends HTTPLocationDecider {
#Inject
private Instance<TestContext> testContext;
#Override
public Scheme canDecide() {
return new Scheme.HTTP();
}
#Override
public String decide(String location) {
String uri = super.decide(location);
// not sure, how reliable this method of getting the current test object is
// if it breaks, there is always a possibility of observing
// org.jboss.arquillian.test.spi.event.suite.TestLifecycleEvent's (or rather its
// descendants) and storing the test object in a ThreadLocal
Object testObject = testContext.get().getActiveId();
if (testObject instanceof LocationParameterProvider) {
Map<String, String> locationParameters =
((LocationParameterProvider) testObject).provideLocationParameters();
StringBuilder uriParams = new StringBuilder(64);
boolean first = true;
for (Entry<String, String> param : locationParameters.entrySet()) {
uriParams.append(first ? '?' : '&');
first = false;
try {
uriParams.append(URLEncoder.encode(param.getKey(), "UTF-8"));
uriParams.append('=');
uriParams.append(URLEncoder.encode(param.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
uri += uriParams.toString();
}
return uri;
}
}
Our LocationDecider must be registered to override the Graphene's one.
import org.jboss.arquillian.core.spi.LoadableExtension;
import org.jboss.arquillian.graphene.location.decider.HTTPLocationDecider;
import org.jboss.arquillian.graphene.spi.location.LocationDecider;
public class MyArquillianExtension implements LoadableExtension {
#Override
public void register(ExtensionBuilder builder) {
builder.override(LocationDecider.class, HTTPLocationDecider.class,
HTTPParameterizedLocationDecider.class);
}
}
MyArquillianExtension should be registered via SPI, so create a necessary file in your test resources, e.g. for me the file path is src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension. The file must contain a fully qualified class name of MyArquillianExtension.
And that's it. Now you can provide location parameters in a test.
import java.util.HashMap;
import java.util.Map;
import org.jboss.arquillian.graphene.page.InitialPage;
import org.jboss.arquillian.graphene.page.Location;
import org.junit.Test;
public class TestyTest implements LocationParameterProvider {
#Override
public Map<String, String> provideLocationParameters() {
Map<String, String> params = new HashMap<>();
params.put("mykey", "myvalue");
return params;
}
#Test
public void test(#InitialPage TestPage page) {
}
#Location("MyTestView.xhtml")
public static class TestPage {
}
}
I've focused on parameters specifically, but hopefully this paves the way for other dynamic path manipulations.
Of course this doesn't fix the Graphene.goTo API. This means before using goTo you have to provide parameters via this roundabout provideLocationParameters way. It's weird. You can make your own alternative API, goTo that accepts parameters, and modify your LocationDecider to support other ParameterProviders.

Cucumber: Unable to find step definition

I have the following feature file: MacroValidation.feature
#macroFilter
Feature: Separating out errors and warnings
Scenario: No errors or warnings when separating out error list
Given I have 0 macros
When I filter out errors and warnings for Macros
Then I need to have 0 errors
And I need to have 0 warnings
My definition files.
package com.test.definition;
import cucumber.api.java.After;
import cucumber.api.java.Before;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import cucumber.runtime.java.StepDefAnnotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.powermock.api.mockito.PowerMockito.doReturn;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.spy;
#StepDefAnnotation
public class MacroValidationStepDefinitions {
private final MacroService macroService = spy(new MacroService());
private final LLRBusList busList = mock(LLRBusList.class);
private final List<String> errorList = new ArrayList<String>();
private final List<String> warningList = new ArrayList<String>();
#Before({"#macroFilter"})
public void setUp() {
errorList.addAll(Arrays.asList("error 1, error2, error 3"));
warningList.addAll(Arrays.asList("warning 1, warning 2, warning 3"));
}
#After({"#macroFilter"})
public void tearDown() {
errorList.clear();
warningList.clear();
}
#Given("^I have (\\d+) macros$")
public void i_have_macros(int input) {
doReturn(input).when(busList).size();
}
#When("^I filtered out errors and warnings for Macros$")
public void i_filtered_out_errors_and_warnings_for_Macros() {
macroService.separateErrorsAndWarning(busList, errorList, warningList);
}
#Then("^I need to have (\\d+) errors$")
public void i_need_to_have_errors(int numOfError) {
if (numOfError == 0) {
assertTrue(errorList.isEmpty());
} else {
assertEquals(errorList.size(), numOfError);
}
}
#Then("^I need to have (\\d+) warnings$")
public void i_need_to_have_warnings(int numOfWarnings) {
if (numOfWarnings == 0) {
assertTrue(warningList.isEmpty());
} else {
assertEquals(warningList.size(), numOfWarnings);
}
}
}
My unit test class.
#CucumberOptions(features = {"classpath:testfiles/MacroValidation.feature"},
glue = {"com.macro.definition"},
dryRun = false,
monochrome = true,
tags = "#macroFilter"
)
#RunWith(Cucumber.class)
public class PageMacroValidationTest {
}
When I execute the test, I get file definition not implemented warnings in the log.
Example log:
You can implement missing steps with the snippets below:
#Given("^I have (\\d+) macros$")
public void i_have_macros(int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
#When("^I filter out errors and warnings for Macros$")
public void i_filter_out_errors_and_warnings_for_Macros() throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
#Then("^I need to have (\\d+) errors$")
public void i_need_to_have_errors(int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
#Then("^I need to have (\\d+) warnings$")
public void i_need_to_have_warnings(int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
I don't think file name should matter right?
It looks like Cucumber isn't finding your step defintion class. In your unit test class you say:
glue = {"com.macro.definition"}
However the step definition classes are in com.test.definition
Try changing that line to:
glue = {"com.test.definition"}
You may have to rebuild your project to pick up the change.
Also, Cucumber is sensitive to white space. If you try to make your runner or feature file pretty after having captured the snippets, you will get this problem.
Here's an example that drove me nuts for several hours while creating my first BDD. I had created the feature file and a skeleton runner which I ran and and captured the snippets. Then I prettified the feature file, and when I ran the runner got the errors.
Of course everything looked fine to my human brain, so the next few hours were spent in fruitless research here, and checking versions and bug lists. Finally I decided to compare the first two lines of the snippet to see what was different:
// #Then("^the test result is = \"([^\"]*)\"$")
// public void theTestResultIs(String ruleResult) throws Throwable {
#Then("^the test result is = \"([^\"]*)\"$")
public void theTestResultIs(String arg1) throws Throwable {
Doh!
Try to use all dependencies in POM.xml with io.cucumber group id which is the latest jars instead of info.cukes. After removing jars update the project with new imports and run the project.
Remember you have to replace all dependencies of info.cukes with io.cucumber

Gradle : how to use BuildConfig in an android-library with a flag that gets set in an app

My (gradle 1.10 and gradle plugin 0.8)-based android project consists of a big android-library that is a dependency for 3 different android-apps
In my library, I would love to be able to use a structure like this
if (BuildConfig.SOME_FLAG) {
callToBigLibraries()
}
as proguard would be able to reduce the size of the produced apk, based on the final value of SOME_FLAG
But I can't figure how to do it with gradle as :
* the BuildConfig produced by the library doesn't have the same package name than the app
* I have to import the BuildConfig with the library package in the library
* The apk of an apps includes the BuildConfig with the package of the app but not the one with the package of the library.
I tried without success to play with BuildTypes and stuff like
release {
// packageNameSuffix "library"
buildConfigField "boolean", "SOME_FLAG", "true"
}
debug {
//packageNameSuffix "library"
buildConfigField "boolean", "SOME_FLAG", "true"
}
What is the right way to builds a shared BuildConfig for my library and my apps whose flags will be overridden at build in the apps?
As a workaround, you can use this method, which uses reflection to get the field value from the app (not the library):
/**
* Gets a field from the project's BuildConfig. This is useful when, for example, flavors
* are used at the project level to set custom fields.
* #param context Used to find the correct file
* #param fieldName The name of the field-to-access
* #return The value of the field, or {#code null} if the field is not found.
*/
public static Object getBuildConfigValue(Context context, String fieldName) {
try {
Class<?> clazz = Class.forName(context.getPackageName() + ".BuildConfig");
Field field = clazz.getField(fieldName);
return field.get(null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
To get the DEBUG field, for example, just call this from your Activity:
boolean debug = (Boolean) getBuildConfigValue(this, "DEBUG");
I have also shared this solution on the AOSP Issue Tracker.
Update: With newer versions of the Android Gradle plugin publishNonDefault is deprecated and has no effect anymore. All variants are now published.
The following solution/workaround works for me. It was posted by some guy in the google issue tracker:
Try setting publishNonDefault to true in the library project:
android {
...
publishNonDefault true
...
}
And add the following dependencies to the app project that is using the library:
dependencies {
releaseCompile project(path: ':library', configuration: 'release')
debugCompile project(path: ':library', configuration: 'debug')
}
This way, the project that uses the library includes the correct build type of the library.
You can't do what you want, because BuildConfig.SOME_FLAG isn't going to get propagated properly to your library; build types themselves aren't propagated to libraries -- they're always built as RELEASE. This is bug https://code.google.com/p/android/issues/detail?id=52962
To work around it: if you have control over all of the library modules, you could make sure that all the code touched by callToBigLibraries() is in classes and packages that you can cleave off cleanly with ProGuard, then use reflection so that you can access them if they exist and degrade gracefully if they don't. You're essentially doing the same thing, but you're making the check at runtime instead of compile time, and it's a little harder.
Let me know if you're having trouble figuring out how to do this; I could provide a sample if you need it.
I use a static BuildConfigHelper class in both the app and the library, so that I can have the packages BuildConfig set as final static variables in my library.
In the application, place a class like this:
package com.yourbase;
import com.your.application.BuildConfig;
public final class BuildConfigHelper {
public static final boolean DEBUG = BuildConfig.DEBUG;
public static final String APPLICATION_ID = BuildConfig.APPLICATION_ID;
public static final String BUILD_TYPE = BuildConfig.BUILD_TYPE;
public static final String FLAVOR = BuildConfig.FLAVOR;
public static final int VERSION_CODE = BuildConfig.VERSION_CODE;
public static final String VERSION_NAME = BuildConfig.VERSION_NAME;
}
And in the library:
package com.your.library;
import android.support.annotation.Nullable;
import java.lang.reflect.Field;
public class BuildConfigHelper {
private static final String BUILD_CONFIG = "com.yourbase.BuildConfigHelper";
public static final boolean DEBUG = getDebug();
public static final String APPLICATION_ID = (String) getBuildConfigValue("APPLICATION_ID");
public static final String BUILD_TYPE = (String) getBuildConfigValue("BUILD_TYPE");
public static final String FLAVOR = (String) getBuildConfigValue("FLAVOR");
public static final int VERSION_CODE = getVersionCode();
public static final String VERSION_NAME = (String) getBuildConfigValue("VERSION_NAME");
private static boolean getDebug() {
Object o = getBuildConfigValue("DEBUG");
if (o != null && o instanceof Boolean) {
return (Boolean) o;
} else {
return false;
}
}
private static int getVersionCode() {
Object o = getBuildConfigValue("VERSION_CODE");
if (o != null && o instanceof Integer) {
return (Integer) o;
} else {
return Integer.MIN_VALUE;
}
}
#Nullable
private static Object getBuildConfigValue(String fieldName) {
try {
Class c = Class.forName(BUILD_CONFIG);
Field f = c.getDeclaredField(fieldName);
f.setAccessible(true);
return f.get(null);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
Then, anywhere in your library where you want to check BuildConfig.DEBUG, you can check BuildConfigHelper.DEBUG and access it from anywhere without a context, and the same for the other properties. I did it this way so that the library will work with all my applications, without needing to pass a context in or set the package name some other way, and the application class only needs the import line changed to suit when adding it into a new application
Edit: I'd just like to reiterate, that this is the easiest (and only one listed here) way to get the values to be assigned to final static variables in the library from all of your applications without needing a context or hard coding the package name somewhere, which is almost as good as having the values in the default library BuildConfig anyway, for the minimal upkeep of changing that import line in each application.
For the case where the applicationId is not the same as the package (i.e. multiple applicationIds per project) AND you want to access from a library project:
Use Gradle to store the base package in resources.
In main/AndroidManifest.xml:
android {
applicationId "com.company.myappbase"
// note: using ${applicationId} here will be exactly as above
// and so NOT necessarily the applicationId of the generated APK
resValue "string", "build_config_package", "${applicationId}"
}
In Java:
public static boolean getDebug(Context context) {
Object obj = getBuildConfigValue("DEBUG", context);
if (obj instanceof Boolean) {
return (Boolean) o;
} else {
return false;
}
}
private static Object getBuildConfigValue(String fieldName, Context context) {
int resId = context.getResources().getIdentifier("build_config_package", "string", context.getPackageName());
// try/catch blah blah
Class<?> clazz = Class.forName(context.getString(resId) + ".BuildConfig");
Field field = clazz.getField(fieldName);
return field.get(null);
}
use both
my build.gradle
// ...
productFlavors {
internal {
// applicationId "com.elevensein.sein.internal"
applicationIdSuffix ".internal"
resValue "string", "build_config_package", "com.elevensein.sein"
}
production {
applicationId "com.elevensein.sein"
}
}
I want to call like below
Boolean isDebug = (Boolean) BuildConfigUtils.getBuildConfigValue(context, "DEBUG");
BuildConfigUtils.java
public class BuildConfigUtils
{
public static Object getBuildConfigValue (Context context, String fieldName)
{
Class<?> buildConfigClass = resolveBuildConfigClass(context);
return getStaticFieldValue(buildConfigClass, fieldName);
}
public static Class<?> resolveBuildConfigClass (Context context)
{
int resId = context.getResources().getIdentifier("build_config_package",
"string",
context.getPackageName());
if (resId != 0)
{
// defined in build.gradle
return loadClass(context.getString(resId) + ".BuildConfig");
}
// not defined in build.gradle
// try packageName + ".BuildConfig"
return loadClass(context.getPackageName() + ".BuildConfig");
}
private static Class<?> loadClass (String className)
{
Log.i("BuildConfigUtils", "try class load : " + className);
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
private static Object getStaticFieldValue (Class<?> clazz, String fieldName)
{
try { return clazz.getField(fieldName).get(null); }
catch (NoSuchFieldException e) { e.printStackTrace(); }
catch (IllegalAccessException e) { e.printStackTrace(); }
return null;
}
}
For me this is the ONLY ONE AND ACCEPTABLE* SOLUTION TO determine the ANDROID APPLICATION BuildConfig.class:
// base entry point
// abstract application
// which defines the method to obtain the desired class
// the definition of the application is contained in the library
// that wants to access the method or in a superior library package
public abstract class BasApp extends android.app.Application {
/*
* GET BUILD CONFIG CLASS
*/
protected Class<?> getAppBuildConfigClass();
// HELPER METHOD TO CAST CONTEXT TO BASE APP
public static BaseApp getAs(android.content.Context context) {
BaseApp as = getAs(context, BaseApp.class);
return as;
}
// HELPER METHOD TO CAST CONTEXT TO SPECIFIC BASEpp INHERITED CLASS TYPE
public static <I extends BaseApp> I getAs(android.content.Context context, Class<I> forCLass) {
android.content.Context applicationContext = context != null ?context.getApplicationContext() : null;
return applicationContext != null && forCLass != null && forCLass.isAssignableFrom(applicationContext.getClass())
? (I) applicationContext
: null;
}
// STATIC HELPER TO GET BUILD CONFIG CLASS
public static Class<?> getAppBuildConfigClass(android.content.Context context) {
BaseApp as = getAs(context);
Class buildConfigClass = as != null
? as.getAppBuildConfigClass()
: null;
return buildConfigClass;
}
}
// FINAL APP WITH IMPLEMENTATION
// POINTING TO DESIRED CLASS
public class MyApp extends BaseApp {
#Override
protected Class<?> getAppBuildConfigClass() {
return somefinal.app.package.BuildConfig.class;
}
}
USAGE IN LIBRARY:
Class<?> buildConfigClass = BaseApp.getAppBuildConfigClass(Context);
if(buildConfigClass !- null) {
// do your job
}
*there are couple of things need to be watched out:
getApplicationContext() - could return a context which is not an App ContexWrapper implementation - see what Applicatio class extends & get to know of the possibilities of context wrapping
the class returned by final app could be loaded by different class loaders than those who will use it - depends of loader implementation and some principals typical (chierarchy, visibility) for loaders
everything depends on the implemmentation of as in this case simple DELEGATION!!! - the solution could be more sophisticetaded - i wanted only to show here the usage of DELEGATION pattern :)
** why i downwoted all of reflection based patterns because they all have weak points and they all in some certain conditions will fail:
Class.forName(className); - because of not speciified loader
context.getPackageName() + ".BuildConfig"
a) context.getPackageName() - "by default - else see b)" returns not package defined in manifest but application id (somtimes they both are the same), see how the manifest package property is used and its flow - at the end apt tool will replace it with applicaton id (see ComponentName class for example what the pkg stands for there)
b) context.getPackageName() - will return what the implementaio wants to :P
*** what to change in my solution to make it more flawless
replace class with its name that will drop the problems wchich could appear when many classes loaded with different loaders accessing / or are used to obtain a final result involving class (get to know what describes the equality between two classes (for a compiler at runtime) - in short a class equality defines not a self class but a pair which is constituted by the loader and the class. (some home work - try load a inner class with different loader and access it by outer class loaded with different loader) - it would turns out that we will get illegal access error :) even the inner class is in the same package has all modificators allowing access to it outer class :) compiler/linker "VM" treats them as two not related classes...