The workspace

A visual layout of the workspace model
Legend
- The primary resource is the input that is targeted for editing. If you drag and drop a single JAR file into Recaf, then this will represent that JAR file. The representation is broken down into pieces...
- The JVM class bundle contains all the
.class
files in the input that are not treated specially by the JVM. - JAR files allow you to have multiple versions of the same class for different JVM versions via "Multi-release JAR". This is a map of JVM version numbers to bundles of classes associated with that specific version.
- Android's APK files may contain multiple containers of classes known as DEX files. This is a mapping of each DEX file to the classes contained within it.
- The file bundle contains all other regular files that are not ZIP archives.
- ZIP archives are represented as embedded resources, allowing a ZIP in a ZIP, or JAR in a JAR, to be navigable within Recaf.
- Workspaces can have multiple inputs. These additional inputs can be used to enhance performance of some services such as inheritance graphing, recompilation, and SSVM virtualization just to name a few. These supporting resources are not intended to be editable and are just there to "support" services as described before.
- Recaf adds a few of its own supporting resources, but manages them separately from the supporting resources list.
- The runtime resource allows Recaf to access classes in the current JVM process like
java.lang.String
. - The android resource allows Recaf to access classes in the Android runtime. It is automatically loaded when a resource with DEX files is detected.
Creating workspaces
To create a Workspace
instance you will almost always be using the BasicWorkspace
implementation. You can pass along either:
- A single
WorkspaceResource
for the primary resource. - A single
WorkspaceResource
for the primary resource, plusCollection<WorkspaceResource>
for the supporting resources.
To create a WorkspaceResource
you can use the ResourceImporter
service, which allows you to read content from a variety of inputs.
Loading workspaces
There are multiple ways to load workspaces internally. Depending on your intent you'll want to do it differently.
For loading from Path
values in a UI context, use PathLoadingManager
. It will handle loading the content from the path in a background thread, and gives you a simple consumer type to handle IO problems.
Otherwise, you can use WorkspaceManager
directly to call setCurrent(Workspace)
.
Exporting workspaces
You can create an instance of WorkspaceExportOptions
and configure them to suite your needs. The options allow you to change:
- The compression scheme of contents.
MATCH_ORIGINAL
which will only compress items if they were originally compressed when read.SMART
which will only compress items if compression yields a smaller output than a non-compressed item. Very small files may become larger with compression due to the overhead of the compression scheme's dictionary.ALWAYS
which always compresses items.NEVER
which never compresses items.
- The output type, being a file or directory.
- The path to write to.
- The option to bundle contents of supporting resources into the exported output.
- The option to create ZIP file directory entries, if the output type is
FILE
. This creates empty entries in the output of ZIP/JAR files detailing directory paths. Some tools may use this data, but its not required for most circumstances.
The configured options instance can be re-used to export contents with the same configuration multiple times. To export a workspace do options.create()
to create a WorkspaceExporter
which then allows you to pass a Workspace
instance.
Listeners
The WorkspaceManager
allows you to register listeners for multiple workspace events.
WorkspaceOpenListener
: When a new workspace is opened within the manager.WorkspaceCloseListener
: When a prior workspace is closed within the manager.WorkspaceModificationListener
: When the active workspace's model is changed (Supporting resource added/removed)
Accessing classes/files in the workspace
Classes and files reside within the WorkspaceResource
items in a Workspace
. You can access the resources directly like so:
// Content the user intends to edit
WorkspaceResource resource = workspace.getPrimaryResource();
// Content to support editing, but is not editable
List<WorkspaceResource> resources = workspace.getSupportingResources();
// All content in the workspace, which may include internally managed
// supporting resources if desired. Typically 'false'.
List<WorkspaceResource> resources = workspace.getAllResources(includeInternal);
As described in the workspace model above, resources have multiple "bundles" that contain content. The groups exist to facilitate modeling a variety of potential input types that Recaf supports. Bundles that represent classes share a common type ClassBundle
and then are broken down further into JvmClassBundle
and AndroidClassBundle
where relevant. Bundles that represent files are only ever FileBundle
.
// Contains JVM classes
JvmClassBundle bundle = resource.getJvmClassBundle();
// Contains JVM classes, grouped by the version of Java targeted by each class
NavigableMap<Integer, VersionedJvmClassBundle> bundles = resource.getVersionedJvmClassBundles();
// Contains Android classes, grouped by the name of each DEX file
Map<String, AndroidClassBundle> bundles = resource.getAndroidClassBundles();
// Contains files
FileBundle bundle = resource.getFileBundle();
// Contains files that represent archives, with a model of the archive contents
Map<String, WorkspaceFileResource> embeddedResources = resource.getEmbeddedResources();
These bundles are Map<String, T>
and Iterable<T>
where T
is the content type.
JvmClassBundle classBundle = resource.getJvmClassBundle();
FileBundle fileBundle = resource.getFileBundle();
// Get JVM class by name (remember to null check)
JvmClassInfo exampleClass = classBundle.get("com/example/Example");
// Looping over bundles
for (JvmClassInfo classInfo : classBundle)
...
for (FileInfo fileInfo : fileBundle)
...
// There are also stream operations to easily iterate over multiple bundles at once.
resource.classBundleStream()
.flatMap(Bundle::stream)
.forEach(classInfo -> {
// All classes in all bundles that hold 'ClassInfo' values
// including JvmClassBundle and AndroidClassBundle instances
});
Workspace PathNodes
There are a lot of portions of Recaf that need to have some way to point to classes and files in the workspace. However, providing just the name of the class or file could be ambiguous if different bundles in the workspace contain entries of the same name. To address this Recaf has a PathNode
type (with many child types) outlining locations in a Workspace
with increasing granularity. For instance, a PathNode
to a ClassInfo
will specify which Bundle
contains that class.
Here is a tree model of the current PathNode
types.
WorkspacePathNode
ResourcePathNode
BundlePathNode
DirectoryPathNode
ClassPathNode
ClassMemberPathNode
AnnotationPathNode
LocalVariablePathNode
InstructionPathNode
ThrowsPathNode
CatchPathNode
InnerClassPathNode
AnnotationPathNode
AnnotationPathNode
FilePathNode
LineNumberPathNode
Finding specific classes/files in the workspace
The Workspace
interface defines some find
operations allowing for simple name look-ups of classes and files.
Method | Usage |
---|---|
ClassPathNode findClass(String internalName) | Finds the first available ClassInfo by the given name, and wraps it in a ClassPathNode . |
ClassPathNode findJvmClass(String internalName) | Finds the first available JvmClassInfo by the given name, and wraps it in a ClassPathNode . |
ClassPathNode findLatestVersionedJvmClass(String internalName) | Finds the most up-to-date JvmClassInfo from all available versioned bundles, wrapping it in a ClassPathNode . |
ClassPathNode findVersionedJvmClass(String internalName, int version) | Finds the first available JvmClassInfo matching the given version (Floored to next available older version), and wraps it in a ClassPathNode |
ClassPathNode findAndroidClass(String internalName) | Finds the first available AndroidClassInfo by the given name, and wraps it in a ClassPathNode . |
DirectoryPathNode findPackage(String name) | Finds the first available ClassInfo defined in the given package, or any sub-package, then wraps the path in a DirectoryPathNode . |
SortedSet<ClassPathNode> findClasses(Predicate<ClassInfo> filter) | Collects all ClassInfo values in the workspace that match the given predicate, and wraps each in a ClassPathNode . The returned set ordering for paths is alphabetic order. |
SortedSet<ClassPathNode> findJvmClasses(Predicate<JvmClassInfo> filter) | Collects all JvmClassInfo values in the workspace that match the given predicate, and wraps each in a ClassPathNode . The returned set ordering for paths is alphabetic order. |
SortedSet<ClassPathNode> findVersionedJvmClasses(Predicate<JvmClassInfo> filter) | Collects all versioned JvmClassInfo values in the workspace that match the given predicate, and wraps each in a ClassPathNode . The returned set ordering for paths is alphabetic order. |
SortedSet<ClassPathNode> findAndroidClasses(Predicate<AndroidClassInfo> filter) | Collects all AndroidClassInfo values in the workspace that match the given predicate, and wraps each in a ClassPathNode . The returned set ordering for paths is alphabetic order. |
FilePathNode findFile(String filePath) | Finds any available FileInfo by the given name, and wraps it in a FilePathNode . |
SortedSet<FilePathNode> findFiles(Predicate<FileInfo> filter) | Collects all FileInfo values in the workspace that match the given predicate, and wraps each in a FilePathNode . The returned set ordering for paths is alphabetic order. |