diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java index eb17a0f4..3efc97a0 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java @@ -22,13 +22,14 @@ package com.webcodepro.applecommander.storage; import com.webcodepro.applecommander.storage.os.prodos.ProdosDirectoryEntry; import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk; import org.applecommander.capability.Capability; -import org.applecommander.filestore.DirectoryEntry; -import org.applecommander.filestore.Entry; +import org.applecommander.filestore.Directory; +import org.applecommander.filestore.DisplayColumn; import org.applecommander.filestore.FileEntry; import org.applecommander.filestore.FileStore; import org.applecommander.util.Container; import java.util.*; +import java.util.function.Function; /** * The DiskFileStoreAdapter is a shim that allows FormattedDisk to be mapped into the @@ -52,8 +53,8 @@ public class DiskFileStoreAdapter implements FileStore { } @Override - public DirectoryEntry getRootDirectory() { - return new DiskDirectoryEntryAdapter(this, null, disk); + public DiskDirectoryAdapter getRootDirectory() { + return new DiskDirectoryAdapter(this, null, disk); } @Override @@ -73,46 +74,73 @@ public class DiskFileStoreAdapter implements FileStore { }; } - public abstract static class DiskEntryAdapter implements Entry { - protected final DiskFileStoreAdapter adapter; - protected final DirectoryEntry parent; - - DiskEntryAdapter(DiskFileStoreAdapter adapter, DirectoryEntry parent) { - this.adapter = adapter; - this.parent = parent; - } - - public DirectoryEntry getParent() { - return parent; - } - public FileStore getFileStore() { - return adapter; + /** + * Translate the "legacy" FormattedDisk FileColumnHeader to the new DisplayColumn structure. + * Note that we need to track what has been seen as well as where it comes from and the index + * values in order to replicate most of the capability. This does not solve for type since the + * old API pre-formats everything as a String. + */ + @Override + public List getDisplayColumns() { + List columns = new ArrayList<>(); + Set alreadySeenKeys = new HashSet<>(); + for (int displayMode : List.of(FormattedDisk.FILE_DISPLAY_NATIVE, + FormattedDisk.FILE_DISPLAY_DETAIL)) { + List headers = disk.getFileColumnHeaders(displayMode); + DisplayColumn.Mode mode = DisplayColumn.Mode.DETAIL; + if (displayMode == FormattedDisk.FILE_DISPLAY_NATIVE) { + mode = DisplayColumn.Mode.NATIVE; + }; + for (int i = 0; i DisplayColumn.Alignment.CENTER; + case FormattedDisk.FileColumnHeader.ALIGN_RIGHT -> DisplayColumn.Alignment.RIGHT; + default -> DisplayColumn.Alignment.LEFT; + }; + if (!alreadySeenKeys.contains(header.getKey())) { + alreadySeenKeys.add(header.getKey()); + final int headerIndex = i; + Function mappingFn = entry -> { + // TODO investigate to see if generics work across these interfaces + if (entry instanceof DiskFileEntryAdapter fileEntryAdapter) { + return fileEntryAdapter.fileEntry.getFileColumnData(displayMode).get(headerIndex); + } + throw new RuntimeException("Unexpected file entry type: " + entry.getClass().getName()); + }; + DisplayColumn displayColumn = new DisplayColumn(header.getTitle(), + alignment, mappingFn::apply, "%s", mode); + columns.add(displayColumn); + } + } } + return columns; } + /** * The DiskFileEntryAdapter is a shim that allows a FileEntry to be mapped into the * new/evolving FileEntry interface(s). */ public static class DiskFileEntryAdapter implements FileEntry { private final DiskFileStoreAdapter adapter; - private final DirectoryEntry parent; + private final DiskDirectoryAdapter parent; private final com.webcodepro.applecommander.storage.FileEntry fileEntry; - private final DirectoryEntry subdirectory; + private final DiskDirectoryAdapter subdirectory; - public DiskFileEntryAdapter(DiskFileStoreAdapter adapter, DirectoryEntry parent, + public DiskFileEntryAdapter(DiskFileStoreAdapter adapter, DiskDirectoryAdapter parent, com.webcodepro.applecommander.storage.FileEntry fileEntry) { this.adapter = adapter; this.parent = parent; this.fileEntry = fileEntry; if (fileEntry instanceof ProdosDirectoryEntry prodosDirectoryEntry) { - this.subdirectory = new DiskDirectoryEntryAdapter(adapter, parent, prodosDirectoryEntry); + this.subdirectory = new DiskDirectoryAdapter(adapter, parent, prodosDirectoryEntry); } else { this.subdirectory = null; } } @Override - public DirectoryEntry getParent() { + public DiskDirectoryAdapter getParent() { return parent; } @Override @@ -168,18 +196,22 @@ public class DiskFileStoreAdapter implements FileStore { * The DiskDirectoryEntryAdapter is a shim that allows a FormattedDisk or DirectoryEntry to be mapped into the * new/evolving DirectoryEntry interface(s). */ - public static class DiskDirectoryEntryAdapter implements DirectoryEntry { + public static class DiskDirectoryAdapter implements Directory { private final DiskFileStoreAdapter adapter; - private final DirectoryEntry parent; + private final DiskDirectoryAdapter parent; private final com.webcodepro.applecommander.storage.DirectoryEntry directoryEntry; - public DiskDirectoryEntryAdapter(DiskFileStoreAdapter adapter, DirectoryEntry parent, - com.webcodepro.applecommander.storage.DirectoryEntry directoryEntry) { + public DiskDirectoryAdapter(DiskFileStoreAdapter adapter, DiskDirectoryAdapter parent, + com.webcodepro.applecommander.storage.DirectoryEntry directoryEntry) { this.adapter = adapter; this.parent = parent; this.directoryEntry = directoryEntry; } @Override + public Optional getParent() { + return Optional.ofNullable(parent); + } + @Override public List getFiles() { try { List entries = new ArrayList<>(); @@ -191,6 +223,10 @@ public class DiskFileStoreAdapter implements FileStore { throw new RuntimeException(e); } } + @Override + public DiskFileStoreAdapter getFileStore() { + return adapter; + } } } diff --git a/lib/ac-api/src/main/java/org/applecommander/filestore/DirectoryEntry.java b/lib/ac-api/src/main/java/org/applecommander/filestore/Directory.java similarity index 74% rename from lib/ac-api/src/main/java/org/applecommander/filestore/DirectoryEntry.java rename to lib/ac-api/src/main/java/org/applecommander/filestore/Directory.java index 629cfbd6..9d9fb993 100644 --- a/lib/ac-api/src/main/java/org/applecommander/filestore/DirectoryEntry.java +++ b/lib/ac-api/src/main/java/org/applecommander/filestore/Directory.java @@ -28,7 +28,15 @@ import java.util.stream.Collectors; * FileStores that don't naturally support a directory do contain a root directory, * so there will be a directory associated to each FileStore. */ -public interface DirectoryEntry { +public interface Directory { + /** + * Get the parent directory. If this is the root directory, it returns empty. + */ + Optional getParent(); + /** + * Returns the FileStore that this Directory belongs to. + */ + FileStore getFileStore(); /** * Return all entries that are stored in this directory. */ @@ -36,10 +44,11 @@ public interface DirectoryEntry { /** * Filter out the DirectoryEntry entries in this directory. */ - default List getDirectories() { + default List getDirectories() { return getFiles().stream() - .filter(fileEntry -> fileEntry instanceof DirectoryEntry) - .map(fileEntry -> (DirectoryEntry)fileEntry) + .map(fileEntry -> fileEntry.get(Directory.class)) + .filter(Optional::isPresent) + .map(Optional::get) .collect(Collectors.toList()); } /** @@ -50,9 +59,9 @@ public interface DirectoryEntry { if (name.equalsIgnoreCase(fileEntry.getName())) { return Optional.of(fileEntry); } - Optional directoryEntry = fileEntry.get(DirectoryEntry.class); - if (directoryEntry.isPresent()) { - Optional finding = directoryEntry.get().findFile(name); + Optional directory = fileEntry.get(Directory.class); + if (directory.isPresent()) { + Optional finding = directory.get().findFile(name); if (finding.isPresent()) { return finding; } diff --git a/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java new file mode 100644 index 00000000..913d98b6 --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java @@ -0,0 +1,60 @@ +/* + * AppleCommander - An Apple ][ image utility. + * Copyright (C) 2026 by Robert Greene and others + * robgreene at users.sourceforge.net + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package org.applecommander.filestore; + +import java.util.Set; +import java.util.function.Function; + +/** + * 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, Function valueFn, String fmt, Mode... modes) { + /** + * Indicates if this DisplayColumn supports the mode. In this manner, the columns + * can be filtered out. Or it can be ignored to display everything. + */ + public boolean supports(Mode mode) { + return Set.of(modes).contains(mode); + } + /** + * Helper method to convert the column into a formatted string for display. + */ + public String formatAsText(FileEntry fileEntry) { + return String.format(fmt, valueFn.apply(fileEntry)); + } + + /** + * Indicates the display mode to help filter columns to users' preference. + */ + public enum Mode { + NATIVE, + DETAIL + } + /** + * Indicates how this column should be formatted. + */ + public enum Alignment { + LEFT, + CENTER, + RIGHT + } +} \ No newline at end of file diff --git a/lib/ac-api/src/main/java/org/applecommander/filestore/FileEntry.java b/lib/ac-api/src/main/java/org/applecommander/filestore/FileEntry.java index 04799075..31cf07e9 100644 --- a/lib/ac-api/src/main/java/org/applecommander/filestore/FileEntry.java +++ b/lib/ac-api/src/main/java/org/applecommander/filestore/FileEntry.java @@ -29,7 +29,7 @@ public interface FileEntry extends Container { /** * The parent DirectoryEntry, if applicable. Can return null. */ - DirectoryEntry getParent(); + Directory getParent(); /** * Indicates if this Entry is deleted. */ diff --git a/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java b/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java index 51e0d448..b9943c68 100644 --- a/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java +++ b/lib/ac-api/src/main/java/org/applecommander/filestore/FileStore.java @@ -22,6 +22,8 @@ package org.applecommander.filestore; import org.applecommander.capability.CapabilityProvider; import org.applecommander.util.Container; +import java.util.List; + /** * A FileStore is a generic interface for all archive and filesystem objects. */ @@ -33,8 +35,10 @@ public interface FileStore extends CapabilityProvider, Container { */ String getLabel(); /** All FileStores support a "root" directory that references all files. */ - DirectoryEntry getRootDirectory(); + Directory getRootDirectory(); /** This is the path separator character. */ // TODO there has to be a better / dynamic way to do this (applies to ProDOS, ShrinkIt only?) String getPathSeparator(); + /** Yields a list of columns to be used in this display mode. */ + List getDisplayColumns(); } diff --git a/lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/DiskFileStoreTest.java b/lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/DiskFileStoreTest.java index 01342fb9..309b7bb9 100644 --- a/lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/DiskFileStoreTest.java +++ b/lib/ac-api/src/test/java/com/webcodepro/applecommander/storage/DiskFileStoreTest.java @@ -21,7 +21,6 @@ package com.webcodepro.applecommander.storage; import org.applecommander.capability.Capability; import org.applecommander.filestore.*; -import org.applecommander.filestore.DirectoryEntry; import org.applecommander.filestore.FileEntry; import org.applecommander.source.Source; import org.applecommander.source.Sources; @@ -248,20 +247,30 @@ public class DiskFileStoreTest { return ctx.fileStores; } - protected void showFiles(DirectoryEntry parent, String indent) { + protected void showFiles(Directory parent, String indent) { + FileStore fileStore = parent.getFileStore(); + List displayColumns = fileStore.getDisplayColumns(); + if (parent.getParent().isEmpty()) { + // Print columns only at the top + System.out.print(indent); + displayColumns.forEach(column -> { + System.out.print(column.headerText()); + System.out.print(" "); + }); + System.out.println(); + } for (FileEntry file : parent.getFiles()) { if (!file.isDeleted()) { - //List data = entry.getFileColumnData(FormattedDisk.FILE_DISPLAY_NATIVE); - //System.out.print(indent); - //for (int d=0; d { + System.out.print(column.formatAsText(file)); + System.out.print(" "); + }); + System.out.println(); + + Optional subdirectory = file.get(Directory.class); + subdirectory.ifPresent(directoryEntry -> showFiles(directoryEntry, indent + " ")); } - Optional subdirectory = file.get(DirectoryEntry.class); - subdirectory.ifPresent(directoryEntry -> showFiles(directoryEntry, indent + " ")); } } @@ -313,9 +322,9 @@ public class DiskFileStoreTest { } } - protected void assertCanReadFiles(DirectoryEntry parent) throws DiskException { + protected void assertCanReadFiles(Directory parent) throws DiskException { for (FileEntry fileEntry : parent.getFiles()) { - Optional subdirectory = fileEntry.get(DirectoryEntry.class); + Optional subdirectory = fileEntry.get(Directory.class); if (fileEntry.isDeleted()) { System.out.printf("Skipping deleted file: %s\n", fileEntry.getName()); }