Gettings method arguments with ASM - java

I found some examples that show me the location of certain method calls using a MethodAdapter:
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
if (owner.equals(targetClass)
&& name.equals(targetMethod.getName())
&& desc.equals(targetMethod.getDescriptor())) {
callsTarget = true;
}
}
I need the arguments, e.g., if I have object.getText("mykey") I would like to get the text "mykey".
Is this possible ?

I have a framework which you may find useful (specifically, procyon-compilertools). It would enable you to do what you ask in a more object-oriented manner than ASM. However, the project is still early in development and subject to change, so I would not recommend using it in a production project.
You could use this as a starting point:
public class CallInspectionSample {
static class Target {
public static void main(String[] args) {
new Target().getText("MyKey");
}
public String getText(final String key) {
return null;
}
}
public static void main(String[] args) {
final TypeReference targetType = MetadataSystem.instance().lookupType("CallInspectionSample$Target");
final TypeDefinition resolvedType = targetType.resolve();
final MethodDefinition mainMethod = resolvedType.getDeclaredMethods().get(1);
final MethodDefinition getTextMethod = resolvedType.getDeclaredMethods().get(2);
final MethodBody mainBody = mainMethod.getBody();
final Block methodAst = new Block();
final DecompilerContext context = new DecompilerContext();
context.setCurrentType(resolvedType);
context.setCurrentMethod(mainMethod);
methodAst.getBody().addAll(AstBuilder.build(mainBody, true, context));
AstOptimizer.optimize(context, methodAst);
for (final Expression e : methodAst.getChildrenAndSelfRecursive(Expression.class)) {
if (e.getCode() == AstCode.InvokeVirtual &&
((MethodReference) e.getOperand()).resolve() == getTextMethod) {
// Analyze arguments here (skip first for instance methods)...
System.out.println(e.getArguments());
}
}
}
}
(example outputs [initobject:Target(Target::<init>), ldc:String("MyKey")])

Related

A way of accessing Windows MMDevice API from Java?

I would like to use the MMDevice API from my Java app. What are my options?
I tried to use JNA. Looks like I can't use JNA Typelib parsing because there no types for this API (Is there a COM type library for Windows Core Audio). As suggested, I need to provide my own declarations of the API.
So I also tried both JNA examples with manual declarations but they give "Interface not supported HRESULT=80004002" error:
public class MMDeviceAPITest {
public static void test1() {
try {
Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED);
var obj = new Test1.MMDeviceEnumerator(); // exception E_NOINTERFACE (HRESULT: 80004002)
// ...
} finally {
Ole32.INSTANCE.CoUninitialize();
}
}
public static void test2() {
try {
Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED);
var factory = new Factory();
var obj = factory.createObject(Test2.MMDeviceEnumerator.class); // exception E_NOINTERFACE (HRESULT: 80004002)
var in = obj.queryInterface(Test2.IMMDeviceEnumerator.class);
// ...
} finally {
Ole32.INSTANCE.CoUninitialize();
}
}
}
interface Test1 {
class MMDeviceEnumerator extends COMLateBindingObject {
public MMDeviceEnumerator() {
super(new Guid.CLSID("bcde0395-e52f-467c-8e3d-c4579291692e"), true);
}
}
}
interface Test2 {
#ComObject(clsId = "bcde0395-e52f-467c-8e3d-c4579291692e")
interface MMDeviceEnumerator extends IUnknown {} // doesn't extend IUnknown in C sources, probably it's the problem...
#ComInterface(iid = "a95664d2-9614-4f35-a746-de8db63617e6")
interface IMMDeviceEnumerator extends IUnknown {}
}
Any ideas how I could access this API from Java? Can I somehow create working declarations for JNA? Or use another framework maybe?
My last idea is to create/find a micro native app/library that wraps the needed COM calls, so I could call this app/library easily (via subprocesses or simple JNA declarations). I'm new to COM world, but it sounds working for me...
The docs you linked show how to create using CoCreateInstance:
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&pEnumerator);
This should get you somewhere close with JNA.
class MMDeviceEnumerator extends Unknown {
public static final CLSID CLSID_MMDeviceEnumerator = new CLSID("bcde0395-e52f-467c-8e3d-c4579291692e");
public static final GUID IID_IMMDeviceEnumerator = new GUID("a95664d2-9614-4f35-a746-de8db63617e6");
public MMDeviceEnumerator(Pointer p) {
super(p);
}
public static MMDeviceEnumerator create() {
PointerByReference pEnumerator = new PointerByReference();
HRESULT hres = Ole32.INSTANCE.CoCreateInstance(
CLSID_MMDeviceEnumerator, null,
WTypes.CLSCTX_ALL, IID_IMMDeviceEnumerator,
pEnumerator);
if (COMUtils.FAILED(hres)) {
return null;
}
return new MMDeviceEnumerator(pEnumerator.getValue());
}
// map functions as needed
}
I used the implementation of IWbemContext in JNA as a template above. You can consult that class for example COM function mappings.
For some reason I can't suggest edits to the answer of Daniel Widdis. The answer worked for me, many thanks! Just wanted to show how to map one method as an example:
class MMDeviceEnumerator extends Unknown {
public static final CLSID CLSID_MMDeviceEnumerator = new CLSID("bcde0395-e52f-467c-8e3d-c4579291692e");
public static final GUID IID_IMMDeviceEnumerator = new GUID("a95664d2-9614-4f35-a746-de8db63617e6");
public MMDeviceEnumerator(Pointer p) {
super(p);
}
public static MMDeviceEnumerator create() {
PointerByReference pEnumerator = new PointerByReference();
HRESULT hres = Ole32.INSTANCE.CoCreateInstance(
CLSID_MMDeviceEnumerator, null,
WTypes.CLSCTX_ALL, IID_IMMDeviceEnumerator, pEnumerator);
if (COMUtils.FAILED(hres)) {
return null;
}
return new MMDeviceEnumerator(pEnumerator.getValue());
}
public static final int EDataFlow_eRender = 0;
public static final int EDataFlow_eCapture = 1;
public static final int EDataFlow_eAll = 2;
public static final int EDataFlow_enum_count = 3;
public static final int DEVICE_STATE_ACTIVE = 0x1;
public static final int DEVICE_STATE_DISABLED = 0x2;
public static final int DEVICE_STATE_NOTPRESENT = 0x4;
public static final int DEVICE_STATE_UNPLUGGED = 0x8;
public static final int DEVICE_STATEMASK_ALL = 0xF;
public void EnumAudioEndpoints(int dataFlow, int dwStateMask, PointerByReference ppDevices) {
WinNT.HRESULT res = (WinNT.HRESULT) _invokeNativeObject(
3, // `EnumAudioEndpoints` is the 3rd method of `IMMDeviceEnumeratorVtbl` in `mmdeviceapi.h`
new Object[] { getPointer(), dataFlow, new WinDef.DWORD(dwStateMask), ppDevices},
WinNT.HRESULT.class
);
COMUtils.checkRC(res);
}
// map other functions as needed
}

Different variants of method depending on parameters

I have classes called ctdl_User, ctdl_Device and ctdl_Options.
I have a function that saves ctdl_User objects using the binary formatter, and another that loads them. However the functions specifically expect to take and return User objects, and I want to use this function to load other objects of mine.
How do I go about changing what types the functions will take? Here is the save and load functions...
public ctdl_User Load()
{
ctdl_User loadedUsr = new ctdl_User();
string DataFileSave = Settings.Default.savePath + "\\testuserfile.dat";
FileStream dataStr = new FileStream(DataFileSave, FileMode.Open);
BinaryFormatter frmtr = new BinaryFormatter();
loadedUsr = (ctdl_User) frmtr.Deserialize(dataStr);
dataStr.Close();
return loadedUsr;
}
public static void Save(ctdl_User usr)
{
string DataFileSave = Settings.Default.savePath + "\\testuserfile.dat";
File.Delete(DataFileSave);
FileStream dataStr = new FileStream(DataFileSave, FileMode.Create);
BinaryFormatter frmtr = new BinaryFormatter();
frmtr.Serialize(dataStr, usr);
dataStr.Close();
}
The following demonstrates an approach using generics in C#:
public static T Load<T>() where T : new()
{
T loadedUsr = new T();
string DataFileSave = Settings.Default.savePath + "\\testuserfile.dat";
FileStream dataStr = new FileStream(DataFileSave, FileMode.Open);
BinaryFormatter frmtr = new BinaryFormatter();
loadedUsr = (T) frmtr.Deserialize(dataStr);
dataStr.Close();
return loadedUsr;
}
public static void Save<T>(T usr)
{
string DataFileSave = Settings.Default.savePath + "\\testuserfile.dat";
File.Delete(DataFileSave);
FileStream dataStr = new FileStream(DataFileSave, FileMode.Create);
BinaryFormatter frmtr = new BinaryFormatter();
frmtr.Serialize(dataStr, usr);
dataStr.Close();
}
Note the use of the new() constraint in the Load() method.
Here's an example of calling these methods:
public static void Main(string[] args)
{
ctdl_User user = new ctdl_User();
user.name = "Alice";
Save<ctdl_User>(user);
ctdl_User user2 = Load<ctdl_User>();
Console.WriteLine(user2.name);
ctdl_Device device = new ctdl_Device();
device.type = "printer";
Save<ctdl_Device>(device);
ctdl_Device device2 = Load<ctdl_Device>();
Console.WriteLine(device2.type);
}
For completeness, here are the stub classes I used to test this code:
[Serializable()]
class ctdl_User
{
public string name;
}
[Serializable()]
class ctdl_Device
{
public string type;
}
Edit: added code to Main() that saves and loads a device, as well as a user.
Assuming this is Java you can just overload the method following this scheme:
public static void Load(Type1 obj)
{
//do sth with object of type Type1
}
public static void Load(Type2 obj)
{
//do sth with object of type Type2
}
//...etc
Also if you have common code for it that can be used for objects of another types you can exrtact this code to one methode that treat overloaded methods as facade
private static void doLoad(Object obj)
{
//the common code
}
public static void Load(Type1 obj)
{
doLoad(obj); // or something else...
}
public static void Load(Type2 obj)
{
doLoad(obj); // or something else...
}
//...etc
You can read more about overloading methods in Java here

How to create and configure Command objects from a dynamic context with variable (number of) arguments?

SO! I've reached an impasse regarding code-design. Here's the scenario.
I am required to either copy, move or delete files. OK, sure, no problem. I can easily write it like this.
public class SimpleFileManager {
public void copy(String sourcePath, String targetPath) { ... }
public void move(String sourcePath, String targetPath) { ... }
public void delete(String filePath) { ... }
}
and call it from a client code in a manner like this, for example:
public static void main(String[] args) {
...
fileManager.copy(x, y);
...
}
However, a request arises that some particular POJOs which have the FileManager reference perform specific operations, depending on some configuration. The actual specification of which POJO instance should do what is contained in config.
Here's an example of what I mean:
public static void main(String[] args) {
...
InvokerPojo invokerPojo1 = new InvokerPojo(invokerPojo1Config, fileManager); // this config tells it to copy files only
InvokerPojo invokerPojo2 = new InvokerPojo(invokerPojo2Config, fileManager); // this config tells it to move files only
InvokerPojo invokerPojo3 = new InvokerPojo(invokerPojo3Config, fileManager); // this config tells it to delete files only
}
So, FileManager provides the functionality to do actual operations, while InvokerPojo simply invokes and delegates those methods based on config.
However, I do not want to be coupled to FileManager, because, for example, I may find some library that provides the same functionality but is much better than mine.
So, I was thinking something like this:
public interface FileManagerDelegator {
void copy(String sourcePath, String targetPath);
void move(String sourcePath, String targetPath);
void delete(String filePath);
}
public class MyFileManagerDelegator implements FileManagerDelegator {
private SimpleFileManager simpleFileManager;
void copy(String sourcePath, String targetPath) { // delegate to simpleFileManager }
void move(String sourcePath, String targetPath) { // delegate to simpleFileManager }
void delete(String filePath) { // delegate to simpleFileManager }
}
public class ComplexFileManagerDelegator implements FileManagerDelegator {
private ComplexFileManager complexFileManager;
void copy(String sourcePath, String targetPath) { // delegate to complexFileManager }
void move(String sourcePath, String targetPath) { // delegate to complexFileManager }
void delete(String filePath) { // delegate to complexFileManager }
}
public interface Command {
void execute();
}
public class CopyCommand() {
private FileManagerDelegator delegator;
String sourcePath;
String targetPath;
void execute() {
delegator.copy(sourcePath, targetPath);
}
}
public class MoveCommand() {
private FileManagerDelegator delegator;
String sourcePath;
String targetPath;
void execute() {
delegator.move(sourcePath, targetPath);
}
}
public class DeleteCommand() {
private FileManagerDelegator delegator;
String filePath;
void execute() {
delegator.delete(filePath);
}
}
public static void main(String[] args) {
...
Command c = getCommand(context);
c.execute();
...
}
Now, the problem is in actually creating that particular command, because I do not want to know which command is being created. All I know is there is info in context that creates it.
I was thinking about having a factory that would create the appropriate Command from context.
The main issue arises in number of parameters for command.
If the only operations that existed were copy and move, then it would have been easy
public interface Command {
void execute(String a, String b);
}
However, since there is a delete operation which takes only one, then I'd either have to have to ignore the other argument in the call or add another method to interface, and I consider both to be bad.
I also don't want to send a variable number of arguments like this:
public interface Command {
void execute(String ... args);
}
because it's just a bit prettier version of this beforemention bad design.
So, would a factory based on context be a bit more cleaner, in a way that my client code doesn't know which operation is being called, and on which receiver:
Example of 2 contexts:
Context copyContext = new Context();
copyContext.set("OPERATION", "COPY");
copyContext.set("sourcePath", sourcePath);
copyContext.set("targetPath", targetPath);
Context deleteContext = new Context();
deleteContext.set("OPERATION", "DELETE");
deleteContext.set("filePath", filePath);
And then, in the factory, I could do something like this:
Command getCommand(FileManagerDelegator delegator, Context context) {
String operation = context.get("OPERATION");
if (operation.equals("COPY")) {
String sourcePath = context.get("sourcePath");
String targetPath = context.get("targetPath");
return new CopyCommand(sourcePath, targetPath, delegator);
} else if (operation.equals("DELETE")) {
String filePath = context.get("filePath");
return new DeleteCommand(filePath, delegator);
} else {
...
}
}
Is there a cleaner, more configurable way to create parametrized command objects on the fly (dynamically configure them, from context) that operate with different (number of) arguments?

Get null value from another object in Java, but get value in own class

When I try to execute this code, after I choose AMD, I got null in value. how it can be happen ?
below is the source code :
[for main]
public class processor{
public int hargapro;
public String nmbarangpro;
public static final Scanner input = new Scanner(System.in);
public String getpro()
{
return nmbarangpro;
}
public int getproharga()
{
return hargapro;
}
public void daftarpro() {
List<String> daftarpro = new ArrayList<>();
daftarpro.add("AMD");
daftarpro.add("Intel");
List<String> nomer = new ArrayList<>();
nomer.add("1. ");
nomer.add("2. ");
System.out.println("Processor yang tersedia :");
for (int i = 0; i < daftarpro.size(); i++) {
System.out.println(nomer.get(i)+daftarpro.get(i));
}
System.out.println("Pilihan anda : ");
int pilih = input.nextInt();
switch(pilih)
{
case 1:
{
System.out.println("Anda membeli Processor AMD");
System.out.println("Seharga Rp 1.200.000");
harga(1200000); //call harga method
namabarang("AMD"); //call namabarang method
System.out.println(getpro()); //[for testing]filled with AMD[ni problem here]
System.out.println(getproharga()); //[for testing][filled with 1200000[no problem here]
break;
}
case 2:
{
System.out.println("Anda membeli Processor AMD");
System.out.println("Seharga Rp 1.200.000");
harga(1500000);
namabarang("Intel");
break;
}
default:
System.out.println("Pilihan tidak tersedia");
daftarpro();
}
}
#Override
public int harga(int hargamasuk) {
return hargapro = hargamasuk;
}
#Override
public String namabarang(String barang) {
return nmbarangpro = barang;
}
public static void main(String[] args) {
processor a = new processor();
a.daftarpro();//get menu from daftarpro()
kasir x = new kasir();
x.semua();//get null in value
}
}
my second files :
public class kasir {
public void semua()
{
processor a = new processor();
System.out.println(a.getpro());
}}
When I try to read value through class kasir, i get x.semua filled with null value. how it can be happen ?
Your semua method creates a new instance of processor which it then reads from:
public void semua()
{
processor a = new processor();
System.out.println(a.getpro());
}
That's entirely unrelated to the processor instance you've created in your main method. If your kasir class should logically "know about" the other processor instance, you probably want a processor field in the class, which you might populate via the constructor - so your main method might become:
public static void main(String[] args) {
processor a = new processor();
a.daftarpro();
kasir x = new kasir(a);
x.semua();
}
As an aside, you should really try to follow the Java naming conventions, so classes of Processor and Kasir, and methods of getPro etc. (And if your code actually looks like that in your editor, I suggest you reformat it, too...)

Java: a variable "might have already been initialized", but I don't understand how

First some context: all the code pasted below is within another class declared as public class TheClass extends SomeProprietaryClass. I cannot declare these classes in another file for various reasons... And log messages are in French. And I'm a "final happy" kind of programmer. Which is at the core of the problem here...
Now, the code... (probably too much of it -- stripping on demand to only keep the relevant parts)
A custom Exception:
private static final class BreadCrumbException
extends Exception
{
private BreadCrumbException(final String message)
{
super(message);
}
private BreadCrumbException(final String message, final Throwable cause)
{
super(message, cause);
}
}
An enum for "materializing" the visibility of a breadcrumb element:
private enum Visibility
{
MAINPAGE("R"),
MENU("M"),
BREADCRUMB("A"),
COMMERCIAL("C");
private static final Map<String, Visibility> reverseMap
= new HashMap<String, Visibility>();
private static final String characterClass;
static {
final StringBuilder sb = new StringBuilder("[");
for (final Visibility v: values()) {
reverseMap.put(v.flag, v);
sb.append(v.flag);
}
sb.append("]");
characterClass = sb.toString();
}
private final String flag;
Visibility(final String flag)
{
this.flag = flag;
}
static EnumSet<Visibility> fromBC(final String element)
{
final EnumSet<Visibility> result = EnumSet.noneOf(Visibility.class);
for (final String s: reverseMap.keySet())
if (element.contains(s))
result.add(reverseMap.get(s));
return result;
}
static String asCharacterClass()
{
return characterClass;
}
static String asString(final EnumSet<Visibility> set)
{
final StringBuilder sb = new StringBuilder();
for (final Visibility v: set)
sb.append(v.flag);
return sb.toString();
}
#Override
public String toString()
{
return flag;
}
}
A breadcrumb element:
private static class BreadCrumbElement
{
private static final Pattern p
= Pattern.compile(String.format("(%s+)(\\d+)",
Visibility.asCharacterClass()));
private final String element;
private final String menuID;
private final EnumSet<Visibility> visibility;
BreadCrumbElement(final String element)
{
final Matcher m = p.matcher(element);
if (!m.matches())
throw new IllegalArgumentException("Élément de fil d'ariane invalide: " + element);
this.element = element;
visibility = EnumSet.copyOf(Visibility.fromBC(m.group(1)));
menuID = m.group(2);
}
public boolean visibleFrom(final Visibility v)
{
return visibility.contains(v);
}
#Override
public boolean equals(final Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
final BreadCrumbElement that = (BreadCrumbElement) o;
return element.equals(that.element);
}
#Override
public int hashCode()
{
return element.hashCode();
}
#Override
public String toString()
{
return element;
}
public String getMenuID()
{
return menuID;
}
}
A breadcrumb:
private static class BreadCrumb
implements Iterable<BreadCrumbElement>
{
private static final BreadCrumb EMPTY = new BreadCrumb();
private final List<BreadCrumbElement> elements
= new LinkedList<BreadCrumbElement>();
private String bc;
BreadCrumb(final String bc)
throws BreadCrumbException
{
final Set<BreadCrumbElement> set = new HashSet<BreadCrumbElement>();
BreadCrumbElement e;
for (final String element: bc.split("\\s+")) {
e = new BreadCrumbElement(element);
if (!set.add(e))
throw new BreadCrumbException("Élément dupliqué "
+ "dans le fil d'Ariane : " + element);
elements.add(e);
}
if (elements.isEmpty())
throw new BreadCrumbException("Fil d'ariane vide!");
if (!elements.get(0).visibleFrom(Visibility.MAINPAGE))
throw new BreadCrumbException("Le fil d'Ariane ne "
+ "commence pas à l'accueil : " + bc);
set.clear();
this.bc = bc;
}
private BreadCrumb()
{
}
BreadCrumb reverse()
{
final BreadCrumb ret = new BreadCrumb();
ret.elements.addAll(elements);
Collections.reverse(ret.elements);
ret.bc = StringUtils.join(ret.elements, " ");
return ret;
}
public Iterator<BreadCrumbElement> iterator()
{
return elements.iterator();
}
#Override
public String toString()
{
return bc;
}
}
The interface to a breadcrumb renderer:
public interface BreadCrumbRender
{
List<CTObjectBean> getBreadCrumb()
throws Throwable;
String getTopCategory();
String getMenuRoot();
String getContext();
}
The implementation of the interface above which is the source of my problems:
private class CategoryBreadCrumbRender
implements BreadCrumbRender
{
private final BreadCrumb bc;
private final CTObject object;
CategoryBreadCrumbRender(final CTObject object)
{
this.object = object;
final String property;
// FIELD_BC is declared as a private static final String earlier on.
// logger is also a private static final Logger
try {
property = object.getProperty(FIELD_BC);
} catch (Throwable throwable) {
logger.fatal("Impossible d'obtenir le champ " + FIELD_BC
+ " de l'objet", throwable);
bc = BreadCrumb.EMPTY;
return;
}
try {
bc = new BreadCrumb(property);
} catch (BreadCrumbException e) {
logger.fatal("Impossible d'obtenir le fil d'Ariane", e);
bc = BreadCrumb.EMPTY; // <-- HERE
}
}
// ....
At the point marked // <-- HERE above, Intellij IDEA, which I use, and javac (1.6.0.29) both tell me that Variable bc might already have been assigned to, which is considered an error (and indeed, the code does not compile).
Trouble is, I do not understand why... My reasoning is the following:
in the first try/catch block (and yes, .getProperty() does throw Throwable), when an exception is caught, bc gets assigned to successfully, and then I return, so far so good;
in the second try/catch block, the constructor may fail, in which case I assign an empty breadcrumb, so it should be OK, even though bc is final: the assignment doesn't happen (?) in the try block but happens in the catch block instead...
Except no, it doesn't. As both IDEA and javac disagree with me, they are certainly right. But why?
(and also, BreadCrumb.EMPTY is declared private static final in the class, I wonder how come I can access it at all... Subsidiary question)
EDIT: there is a known bug with the final keyword (here, thanks to #MiladNaseri for linking to it), however it should be noted that in this bug, variable v is only ever assigned in catch blocks -- but in the code above, I assign it in try blocks and only assign it in catch blocks if an exception is thrown. Also, it should be noted that the error only occurs in the second catch block.
Okay, suppose that in the first try block, when doing property = object.getProperty(FIELD_BC); an exception occurs. So, JVM will enter the catch block, and initialize bc along the way.
Then in the second try block, also an exception occurs, resulting in BreadCrumb.EMPTY being assigned to bc, effectively overriding its original value.
Now, that is how bc might have already been initialized. I hope you see where I'm coming from.
Since the JAVAC analysis engine does not draw a distinction between one or many statements inside the try block, it does not see your case any different than the below:
try {
bc = null;
String x = null;
System.out.println(x.toString());
} catch (Throwable e) {
bc = null;
}
In which case, bc will be assigned twice. In other words, JAVAC won't care that where the source of the Throwable lies, it only cares that it can be there, and that bc might undergo a successful assignment in that try block.
I don't think the analysis is deep enough to really understand that there is only one statement in the try block, and the diagnostic is issued no matter what, so that's why you're seeing it in your case.
Try this instead:
BreadCrumb tmp = null;
try {
tmp = new BreadCrumb(property);
} catch (BreadCrumbException e) {
logger.fatal("Impossible d'obtenir le fil d'Ariane", e);
tmp = BreadCrumb.EMPTY;
}
bc = tmp;

Categories

Resources