Skip to content

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):

  • FileStore will be replacing FormattedDisk. There currently exists a shim to map the old FormattedDisk APIs into FileStore while they get migrated.
  • Directory is a read-only directory while WritableDirectory is a directory that supports file or directory creation. The capabilities are outlined by the CapabilityProvider interface.
  • FileEntry is a read-only directory that supports resource forks while WritableFileEntry is a file entry that has write capability.
  • ByteArrayImageLayout was replaced with Source.
  • Source discovery mechanism is now in Source.Factory.
  • Sources#create(Object) is now used to create Source objects. Sometimes with a special Source. There are two Sources during the transition. One in com.webcodepro.applecommander.storage which will unwrap a SHK/SDK into a ProDOS FormattedDisk. The Sources in org.applecommander.source will instead allow discovery of the ShrinkitFileStore. Use the one appropriate to your needs.
  • Disk was removed. After discovery was removed, it was just a placeholder and all functionality was merged into FormattedDisk.
  • Constants from Disk are now in DiskConstants.
  • FormattedDisk discovery mechanism is now in DiskFactory.
  • Disks#inspect(Source) is now used to initiate the FormattedDisk discovery mechanism.
  • ImageOrder was replaced by Device. Specific cases are BlockDevice and TrackSectorDevice.
  • Geometry are embedded records in the BlockDevice and TrackSectorDevice.
  • DisplayColumn is a somewhat more type-safe display column mechanism. (The prior incarnation was all strings and more work to setup.)
  • ProdosAttributes is a class that encapsulates a files details as ProDOS attributes. It also includes (optionally) the data. The ProdosAttributes can be used, for example, in creating a file. If the ProdosAttributes is fully populated (eg, has the file data), the create call to Directory creates, names, translates the filetype, and writes file data in one operation.
  • FileMagic is 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:

  1. Create a Source via Sources.
  2. Use Disks to "discover" a list of FormattedDisk objects.

Accessing a file store

With the migration from FormattedDisk into a more generic FileStore, the discovery and creation mechanism remain similar:

  1. Create a Source via Sources.
  2. Use FileStores to discover a list of FileStore objects.

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.

CapabilityProviderboolean can(Capability capability);HintProviderboolean is(Hint hint);default boolean isAny(Hint... hints);Container<T> Optional<T> get(Class<T> iface);InformationProviderList<InformationGroup> information();Sourceint getSize();String getName();DataBuffer readAllBytes();DataBuffer readBytes(int offset, int length);void writeBytes(int offset, DataBuffer data);boolean hasChanged();void clearChanges();void save();default boolean isApproxEQ(int value);default boolean isApproxLE(int value);default boolean extensionLike(String extension);FileSourceUniversalDiskImageDiskCopyImageDataBufferSourceFileEntrySource
CapabilityProviderboolean can(Capability capability);HintProviderboolean is(Hint hint);default boolean isAny(Hint... hints);Container<T> Optional<T> get(Class<T> iface);InformationProviderList<InformationGroup> information();Sourceint getSize();String getName();DataBuffer readAllBytes();DataBuffer readBytes(int offset, int length);void writeBytes(int offset, DataBuffer data);boolean hasChanged();void clearChanges();void save();default boolean isApproxEQ(int value);default boolean isApproxLE(int value);default boolean extensionLike(String extension);FileSourceUniversalDiskImageDiskCopyImageDataBufferSourceFileEntrySource

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.

DiskFactoryvoid inspect(Context ctx);ProdosDiskFactoryNakedosDiskFactoryCpmDiskFactoryPascalDiskFactoryGutenbergDiskFactoryDosDiskFactoryRdosDiskFactory
DiskFactoryvoid inspect(Context ctx);ProdosDiskFactoryNakedosDiskFactoryCpmDiskFactoryPascalDiskFactoryGutenbergDiskFactoryDosDiskFactoryRdosDiskFactory

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.

FileStoreFactoryvoid inspect(Context ctx);DiskFileStoreFactoryGamedosFileStoreFactoryShrinkitFileStoreFactoryZipFileStoreFactory
FileStoreFactoryvoid inspect(Context ctx);DiskFileStoreFactoryGamedosFileStoreFactoryShrinkitFileStoreFactoryZipFileStoreFactory

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)));
    }
}