Updated API and UML
API changes
As of AppleCommander 12, the API is starting to be redesigned. This page will capture what the "new world" looks like if you are a developer using the AppleCommander APIs.
Changes (so far):
FileStorewill be replacingFormattedDisk. There currently exists a shim to map the oldFormattedDiskAPIs intoFileStorewhile they get migrated.Directoryis a read-only directory whileWritableDirectoryis a directory that supports file or directory creation. The capabilities are outlined by theCapabilityProviderinterface.FileEntryis a read-only directory that supports resource forks whileWritableFileEntryis a file entry that has write capability.ByteArrayImageLayoutwas replaced withSource.Sourcediscovery mechanism is now inSource.Factory.Sources#create(Object)is now used to createSourceobjects. Sometimes with a specialSource. There are twoSourcesduring the transition. One incom.webcodepro.applecommander.storagewhich will unwrap a SHK/SDK into a ProDOSFormattedDisk. TheSourcesinorg.applecommander.sourcewill instead allow discovery of theShrinkitFileStore. Use the one appropriate to your needs.Diskwas removed. After discovery was removed, it was just a placeholder and all functionality was merged intoFormattedDisk.- Constants from
Diskare now inDiskConstants. FormattedDiskdiscovery mechanism is now inDiskFactory.Disks#inspect(Source)is now used to initiate theFormattedDiskdiscovery mechanism.ImageOrderwas replaced byDevice. Specific cases areBlockDeviceandTrackSectorDevice.Geometryare embedded records in theBlockDeviceandTrackSectorDevice.DisplayColumnis a somewhat more type-safe display column mechanism. (The prior incarnation was all strings and more work to setup.)ProdosAttributesis a class that encapsulates a files details as ProDOS attributes. It also includes (optionally) the data. TheProdosAttributescan be used, for example, in creating a file. If theProdosAttributesis fully populated (eg, has the file data), the create call toDirectorycreates, names, translates the filetype, and writes file data in one operation.FileMagicis an attempt to consolidate the "magic" in file identification.
Accessing a disk image
With the removal of Disk as the discovery and creation mechanism, there are a couple of replacement hooks to be aware of:
A FormattedDisk originates from a Source (new) and is created via the discovery mechanism supported by Disks.
That is:
- Create a
SourceviaSources. - Use
Disksto "discover" a list ofFormattedDiskobjects.
Accessing a file store
With the migration from FormattedDisk into a more generic FileStore, the discovery and creation mechanism remain similar:
- Create a
SourceviaSources. - Use
FileStoresto discover a list ofFileStoreobjects.
Sources
A Source, which replaces the layout, does some of the same stuff but supports discovery and complex types (such as 2IMG and DiskCopy).
Note that a source can wrap around another source, allowing a constant GZip support or data source (such as a FileEntry on an image
or a Shrinkit archive).
Source source = Sources.create(filename).orElseThrow();
To create a source, pass in a String, File, Path, or another Source to the Sources factory. The create(...) method
returns an Optional<Source> just in case -- mostly, you can ignore the optional part and just retrieve the object.
This is the interface for a Source factory. Note that in the first pass, a Source is identified via fromObject(Object)
and in the second pass is identified via fromSource(Source). The FileSource is identified in the first round (handling GZip as well);
while the DiskCopyImage and UniversalDiskImage is identified via the second mechanism. Note that the FileEntry source
comes in programmatically (as in via a double-click in the GUI) and DataBufferSource is used for blank (new) images.
interface Source.Factory {
Optional<Source> fromObject(Object object);
Optional<Source> fromSource(Source source);
}
Disks
To identify disks, a DiskFactory has been created that allows every type of disk to have its own dedicated discovery mechanism.
Prior to this, all that logic was mashed into the (old) Disk class and partially handled in the constructor and then handled
in the getFormattedDisks method itself. The intent of a DiskFactory is to get that logic out of the Disk and into a
dedicated filesystem (or other) discovery mechanism.
Harnessing the DiskFactory is rather straight-forward. Use Disks#inspect(Source) to initiate discovery. For example:
Source source = Sources.create(imageName).orElseThrow();
DiskFactory.Context ctx = Disks.inspect(source);
This will hand back a Context. This contains all the information used to create FormattedDisks, including the Source,
and any NibbleTrackReaderWriters should that be applicable.
public class Context {
public final Source source;
public final NibbleTrackReaderWriter nibbleTrackReaderWriter;
public final List<FormattedDisk> disks = new ArrayList<>();
public BlockDeviceBuilder blockDevice();
public TrackSectorDeviceBuilder trackSectorDevice();
}
public class BlockDeviceBuilder {
public BlockDeviceBuilder include16Sector(Hint hint);
public BlockDeviceBuilder include800K();
public BlockDeviceBuilder includeHDV();
public List<BlockDevice> get();
}
public class TrackSectorDeviceBuilder {
public TrackSectorDeviceBuilder include13Sector();
public TrackSectorDeviceBuilder include16Sector(Hint hint);
public List<TrackSectorDevice> get();
}
Most of the time, disks is what you'll need. If nothing is recognized, disks will be empty, however, the device
builder may be used to create an appropriate device. If applicable, a NibbleTrackReaderWriter will be avilable (either *.nib
or a *.woz image). Of course, Source is what is handed into the Disks#inspect(Source) method.
Examples of using the builder interfaces:
List<TrackSectorDevice> sectorDevices = ctx.trackSectorDevice()
.include13Sector()
.include16Sector(Hint.DOS_SECTOR_ORDER)
.get()
List<BlockDevice> blockDevices = ctx.blockDevice()
.include16Sector(Hint.PRODOS_BLOCK_ORDER)
.include800K()
.includeHDV()
.get();
FileStores
One of the "issues" with the FormattedDisk is the assumption that we are dealing with a disk. Hence the clever
implementation of ShrinkIt support (this was contributed, it is indeed clever). Which also explains when a SHK/SDK
is opened in AppleCommander, it looks just like a ProDOS disk image. Because it is.
A FileStore no longer assumes a disk. It just assumes that we have a "thing" that provides storage of files. (Naming is hard.) They have a directory, which provides a list of files. And a bunch of file entries which are what most people are interested in.
Much of the components related to disks are discoverable via the Container#get mechanism. At this point, a disk image
may support a Source, Usage or Device information. Both of these also supply a Geometry object. In the case
of an archive, only the Source object applies.
There is a shim class called DiskFileStoreAdapter that wraps around a FormattedDisk, so while these get migrated
over to a FileStore, they will still be available.
DataBuffer
A lot of functionality was encompassed in AppleUtil. As an experiment, DataBuffer has been created to wrap around that
functionality (reading unsigned bytes, unsigned words, 32-bit integers, handling little-ending vs big-endian numbers, etc).
That DataBuffer can "get" from a specified offset, or can "read" in a forward manner. Usage depends on what is needed.
CapabilityProvider
The concept of a capability is "what can this -thing- do?" This is still early and won't really show up until devices are put together. But you might as if this "thing" can "write" or "save to disk". Instead of having a bunch of dedicated "is" methods, this should help clean up the API.
For example these are the current capabilities that can be queried: FORMAT_TRACK, WRITE_SECTOR, WRITE_BLOCK,
SAVE_SOURCE, CREATE_FILES, DELETE_FILES, WRITE_FILES, SUPPORTS_DIRECTORIES, CREATE_DIRECTORIES,
SUPPORTS_RESOURCE_FORKS.
public interface CapabilityProvider {
boolean can(Capability capability);
}
Expected usage:
FormattedDisk disk = ...;
if (disk.can(Capability.SAVE_SOURCE)) {
window.enableSaveButton();
}
HintProvider
"Hints" are used to indicate somewthing we know. In particular, a Hint is used while creating the disk image. One example
is if we know the sector ordering for a disk. We can provide that hint for certain sources (DiskCopy, 2IMG, and the current
FileEntry sources do this).
public interface HintProvider {
boolean is(Hint hint);
default boolean isAny(Hint... hints);
}
Example usage:
if (source.is(Hint.PRODOS_BLOCK_ORDER)) {
// ...
}
Container
A "Container" allows pulling out internal components without providing a bunch of random "get" methods. For instance, the current
FormattedDisk (and Disk before that) as well as (the retired) ImageOrder all support both block and track/sector reading and writing.
So, a ProDOS disk has reading by block or sector. DOS has reading by block or sector, etc. RDOS has, not only block or sector,
but also "RDOS" block (256) bytes; and CP/M is the same except the CP/M block is 1024 bytes. It would be nice to hide all that
cruft and "ask" the (in this case) file system if it supported a particular type of device. That's where containers come into
play -- we expose one "get" method that then looks for that type of item. It returns a Java Optional, so it may not and we
can handle that accordingly.
public interface Container {
<T> Optional<T> get(Class<T> iface);
}
Expected usage:
Optional<BlockDevice> blockOpt = disk.get(BlockDevice.class);
blockOpt.ifPresent(device -> {
DataBuffer data = device.readBlock(0);
// ...
});
Information
A lot of things have extra information about them. DiskCopy has image type, disk name, and disk format (for instance) while
2IMG supports ordering, creator, and comments. These are available through a generic information() method call.
public record InformationGroup(String title, List<InformationItem> items) {
}
public record InformationItem(String label, String value) {
}
Note that the Information record includes a builder to aid in construction.
@Override
public List<InformationGroup> information() {
return InformationGroup.builder("Track/Sector to Block Adapter")
.item("Block Style").value(style.name())
.item("Block Size (Bytes)").value(style.blockSize)
.item("Sectors per Block").value(style.sectorsPerBlock)
.get(geometry, device);
}
DisplayColumn
A DisplayColumn supports formatted output from various FileStores in a dynamic fashion. The intent is to provide
a flexible display interface that does not rely on hard-coded implementations.
public record DisplayColumn(String headerText, Alignment alignment, DataType dataType,
Function<FileEntry,Object> getValueFn, BiConsumer<FileEntry,Object> setValueFn,
boolean editInline, String fmt, Mode... modes) {
<snip>
}
Note that it only supports a native and detail mode; alignment can be the expected left, right, and center, and the datatypes (as of now) are string, integer, long, and double (typically percentages).
As an example of setup, this is the ZipFileStore#getDisplayColumns method:
@Override
public List<DisplayColumn> getDisplayColumns() {
return DisplayColumn.builder(ZipFileEntry.class)
.addLongField("Length", FileEntry::getSize)
.addStringField("Method", ZipFileEntry::getMethodName, Mode.DETAIL)
.addLongField("Size", ZipFileEntry::getCompressedSize, Mode.DETAIL)
.addPercentField("Ratio", ZipFileEntry::getCompressedSize, ZipFileEntry::getSize, "%2.0f%%", Mode.DETAIL)
.addFileTimeField("Creation", ZipFileEntry::getCreationTime)
.addFileTimeField("Last Modified", ZipFileEntry::getLastModifiedTime)
.addLongField("CRC-32", ZipFileEntry::getCrc, "%08x", Mode.DETAIL)
.addStringField("Name", FileEntry::getName)
.toList();
}
Usage is something like this:
FileStore fileStore = ...
for (FileEntry fileEntry : fileStore.getRootDirectory().getFiles()) {
for (DisplayColumn column : fileStore.getDisplayColumns()) {
System.out.printf("%s = %s\n", column.headerText(),
String.format(column.fmt(), column.getValueFn().apply(fileEntry)));
}
}