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

  • 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.
  • 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.

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.

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);Sourceint getSize();String getName();DataBuffer readAllBytes();DataBuffer readBytes(int offset, int length);void writeBytes(int offset, DataBuffer data);boolean hasChanged();void clearChanges();List<Information> information();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);Sourceint getSize();String getName();DataBuffer readAllBytes();DataBuffer readBytes(int offset, int length);void writeBytes(int offset, DataBuffer data);boolean hasChanged();void clearChanges();List<Information> information();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 ImageOrders that were considered.

class Context {
    public final Source source;
    public final List<ImageOrder> orders = new ArrayList<>();
    public final List<FormattedDisk> disks = new ArrayList<>();
}

Most of the time, disks is what you'll need. If nothing is recognized, disks will be empty, however orders will have the ImageOrders considered. Of course, Source is what is handed into the Disks#inspect(Source) method.

Other (semi-experimental)

Some of these are in play today (DataBuffer, HintProvider, and Information) and others are partly implemented (CapabilityProvider and Container).

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.

public interface CapabilityProvider {
    boolean can(Capability capability);
}

Expected usage:

FormattedDisk disk = ...;
if (disk.can(Capability.SAVE_TO_FILE)) {
    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 ImageOrder all support both block and track/sector reading and writing. So, a ProDOS disk has reading by block or sector. DOS hsa 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.isPresent(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 getInformation() method call.

public record Information(String label, String value) {
}

Note that the Information record includes a builder to aid in construction.

@Override
public List<Information> information() {
    List<Information> list = source.information();
    // ...
    list.add(Information.builder("Image Type").value("Universal Disk Image (2IMG/2MG)"));
    list.add(Information.builder("ProDOS Blocks").value(info.prodosBlocks()));
    list.add(Information.builder("Data Offset & Length").value("$%08x & %08x",
            info.dataOffset(), info.dataLength()));
    // ...
}