Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
How can I set a private class instance variable to the File object used by an unmarshaller?
(Brought over from SE)
The problem
I'm coding a Java Swing application dealing with XML files, so I'm using JAXB in order to marshal classes into documents and unmarshal the other way around.
I want to include a private field in the class that gets marshalled, which will store the backing file the class is based on (if any), in the form of a File
object. This way, I can determine if a backing file is in use, so when saving via a Save command, if the backing file is available, I can just marshal the class directly to that file, instead of obtaining it via a "Save file" dialog.
However, it seems that with the tools available in JAXB, I cannot get the File
object from the Unmarshaller
while opening it. How can I tackle the situation so that I can set that variable correctly?
As this variable is internal, I don't want to include a setter or expose it so that other classes can't change it.
Background
Being aware of class event callbacks and external listeners, I know I can use a class event callback to set a class instance private field either before or after unmarshalling, but it seems I can't retrieve the file object being in use by the Unmarshaller
from inside that callback.
On the other hand, with an external listener I could get ahold of the File
object being unmarshalled, as it would be at the same level with the unmarshal
method call, but now the private field would either need to be public or include a setter in order for it to be set.
Sample code
The following is a minimal, reproducible example, split in two files: JAXBTest.java
and MarshalMe.java
, both placed at the same level.
MarshalMe.java
import java.io.File;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class MarshalMe {
private File backingFile;
public File getBackingFile() {
return backingFile;
}
// Dummy function that sets the backing file beforehand.
public void processSth() {
backingFile = new File("dummy.hai");
}
}
JAXBDemo.java
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
public class JAXBTest {
public static void writeXML(MarshalMe me, File xml) {
try {
JAXBContext contextObj = JAXBContext.newInstance(MarshalMe.class);
Marshaller marshallerObj = contextObj.createMarshaller();
marshallerObj.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshallerObj.marshal(me, new FileOutputStream(xml));
} catch (JAXBException jaxbe) {
jaxbe.printStackTrace();
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
}
public static MarshalMe readXML(File xml) {
MarshalMe me = null;
try {
JAXBContext contextObj = JAXBContext.newInstance(MarshalMe.class);
Unmarshaller unmarshallerObj = contextObj.createUnmarshaller();
me = (MarshalMe) unmarshallerObj.unmarshal(xml);
} catch (JAXBException jaxbe) {
jaxbe.printStackTrace();
}
return me;
}
public static void main(String[] args) {
MarshalMe src = new MarshalMe();
src.processSth();
System.out.println(src.getBackingFile());
File meFile = new File("me.xml");
writeXML(new MarshalMe(), meFile);
MarshalMe retrieved = readXML(meFile);
System.out.println(retrieved.getBackingFile());
}
}
Expected output
Running with Java 1.8 (or later, provided a JAXB library and runtime implementation is accesible), the minimal, reproducible example outputs:
dummy.hai
null
when I expect the output to be
dummy.hai
me.xml
as the class is initially written in a XML file named me.xml
before being read back.
1 answer
I've found a way to set the private field without exposing it or giving it a setter: Reflection.
Using external event listeners, I can get ahold of the File
object. Then, inside the beforeUnmarshal
method, and after checking that I got the correct object, I use reflection to get the private field, and with the setAccessible
method, I can now control when I get access to the field using reflection only.
After lifting the access checks, it's only a matter of editing the value via set
ting it, and reinstating the checks after that.
Relevant changes
The following snippet includes the relevant changes:
unmarshallerObj.setListener(new Unmarshaller.Listener() {
@Override
public void beforeUnmarshal(Object target, Object parent) {
if (!(target instanceof MarshalMe))
return;
MarshalMe me = (MarshalMe) target;
try {
Field meBackingFile = MarshalMe.class.getDeclaredField("backingFile");
meBackingFile.setAccessible(true);
meBackingFile.set(me, xml);
meBackingFile.setAccessible(false);
} catch (NoSuchFieldException | IllegalAccessException ex) {
// Intentionally left blank.
}
}
});
Including the edit in the sample program
Edit the file JAXBDemo.java
by adding the following code:
// Add to the import section
import java.lang.reflect.Field;
// Under this function
public static MarshalMe readXML(File xml) {
MarshalMe me = null;
try {
JAXBContext contextObj = JAXBContext.newInstance(MarshalMe.class);
Unmarshaller unmarshallerObj = contextObj.createUnmarshaller();
/* Add this code vvv */
unmarshallerObj.setListener(new Unmarshaller.Listener() {
@Override
public void beforeUnmarshal(Object target, Object parent) {
if (!(target instanceof MarshalMe))
return;
MarshalMe me = (MarshalMe) target;
try {
Field meBackingFile = MarshalMe.class.getDeclaredField("backingFile");
meBackingFile.setAccessible(true);
meBackingFile.set(me, xml);
meBackingFile.setAccessible(false);
} catch (NoSuchFieldException | IllegalAccessException ex) {
// Intentionally left blank.
}
}
});
/* Add this code ^^^ */
me = (MarshalMe) unmarshallerObj.unmarshal(xml);
} catch (JAXBException jaxbe) {
jaxbe.printStackTrace();
}
return me;
}
After adding the import
and the code between the /* Add this code */
lines, running the program again now outputs:
dummy.hai
me.xml
as expected.
0 comment threads