001package com.studentgui.apptheming;
002
003import java.awt.Color;
004import java.awt.Graphics2D;
005import java.awt.RenderingHints;
006import java.awt.event.ActionEvent;
007import java.awt.event.InputEvent;
008import java.awt.event.KeyEvent;
009import java.awt.image.BufferedImage;
010import java.io.File;
011import java.io.IOException;
012import java.net.JarURLConnection;
013import java.net.URI;
014import java.net.URL;
015import java.net.URLConnection;
016import java.util.ArrayList;
017import java.util.Enumeration;
018import java.util.List;
019import java.util.jar.JarEntry;
020import java.util.jar.JarFile;
021
022import javax.swing.AbstractAction;
023import javax.swing.ImageIcon;
024import javax.swing.JMenu;
025import javax.swing.JMenuBar;
026import javax.swing.JMenuItem;
027import javax.swing.KeyStroke;
028
029import com.studentgui.app.Main;
030
031/**
032 * Application theming and menu bar construction utilities.
033 *
034 * <p>Provides centralized menu bar factory for the main application window with
035 * keyboard shortcuts, mnemonics, and accessibility support. The menu structure
036 * organizes assessment pages into logical categories:</p>
037 *
038 * <ul>
039 *   <li><b>Navigate Menu:</b> Primary navigation menu containing:
040 *     <ul>
041 *       <li><b>Home:</b> Returns to homepage (Ctrl+Alt+H)</li>
042 *       <li><b>Tactile Submenu:</b> Braille and Abacus skills pages (alphabetical)</li>
043 *       <li><b>Technology Submenu:</b> Device-specific pages (BrailleNote, BrailleSense, iOS, ScreenReader, etc.)</li>
044 *       <li><b>Communication Submenu:</b> Contact Log and Session Notes</li>
045 *       <li><b>Other Skills Submenu:</b> CVI, Digital Literacy, Keyboarding, Observations, Instructional Materials</li>
046 *     </ul>
047 *   </li>
048 * </ul>
049 *
050 * <p><b>Accessibility Features:</b></p>
051 * <ul>
052 *   <li>All menu items include accessible names and descriptions</li>
053 *   <li>Keyboard shortcuts use Ctrl+Alt+Letter combinations to avoid conflicts</li>
054 *   <li>Mnemonics provided for primary menu items (Alt+H for Home, etc.)</li>
055 *   <li>Color-coded icons generated programmatically via {@link #makeIcon(Color, int)}</li>
056 * </ul>
057 *
058 * <p><b>Icon Generation:</b> Menu items display small colored square icons for
059 * visual differentiation. Icons are generated at runtime as 12×12px {@link BufferedImage}
060 * instances with anti-aliased rendering for smooth appearance across themes.</p>
061 *
062 * <p><b>Menu Structure Rationale:</b></p>
063 * <ul>
064 *   <li>Tactile skills (Braille, Abacus) grouped separately from technology devices</li>
065 *   <li>Technology submenu organized by device type (notetakers, mobile OS, desktop screen readers)</li>
066 *   <li>Communication tools (Contact Log, Session Notes) kept together for workflow consistency</li>
067 *   <li>Remaining assessment pages grouped under "Other Skills" for flexibility</li>
068 * </ul>
069 *
070 * <p><b>Navigation Integration:</b> All menu items invoke the main navigation logic in {@link com.studentgui.app.Main}
071 * to switch the main content panel. Page identifiers are lowercase strings matching page class names
072 * (e.g., "braille", "abacus", "braillenote").</p>
073 *
074 * <p><b>Theme Management:</b> Currently limited to menu bar construction. Future expansion
075 * may include FlatLaf theme switching, custom color schemes, or icon set selection.</p>
076 *
077 * @see com.studentgui.app.Main
078 * @see javax.swing.JMenuBar
079 * @see javax.swing.JMenu
080 * @see javax.swing.JMenuItem
081 */
082public class Theme {
083    /**
084     * Build and return the application menu bar used in the main frame.
085     *
086     * @return a {@link JMenuBar} instance containing the application's menus
087     */
088    public static JMenuBar createMenuBar() {
089        JMenuBar mb = new JMenuBar();
090        JMenu nav = new JMenu("Navigate");
091
092        // Home
093        JMenuItem home = new JMenuItem(new AbstractAction("Home") {
094            @Override
095            /**
096             * Handle the Home menu item selection by navigating the main frame
097             * to the application homepage.
098             *
099             * @param e the ActionEvent triggered by selecting the Home menu item
100             */
101
102            public void actionPerformed(final ActionEvent e) { Main.showPage("homepage", null); }
103        });
104        home.setMnemonic('H');
105        home.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
106        home.setIcon(makeIcon(new Color(0x4A90E2), 12));
107        home.getAccessibleContext().setAccessibleName("Home");
108        home.getAccessibleContext().setAccessibleDescription("Open the Home page");
109        nav.add(home);
110        nav.addSeparator();
111
112        // Tactile section (alphabetical)
113        JMenu tactile = new JMenu("Tactile");
114        JMenuItem abacus = new JMenuItem(new AbstractAction("Abacus") {
115            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("abacus", null); }
116        });
117        abacus.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
118        abacus.setIcon(makeIcon(new Color(0xF5A623), 12));
119        abacus.getAccessibleContext().setAccessibleName("Abacus");
120        abacus.getAccessibleContext().setAccessibleDescription("Open the Abacus skills page");
121        tactile.add(abacus);
122
123        JMenuItem braille = new JMenuItem(new AbstractAction("Braille") {
124            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braille", null); }
125        });
126        braille.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
127        braille.setIcon(makeIcon(new Color(0x50E3C2), 12));
128        braille.getAccessibleContext().setAccessibleName("Braille");
129        braille.getAccessibleContext().setAccessibleDescription("Open the Braille skills page");
130        tactile.add(braille);
131
132        nav.add(tactile);
133        nav.addSeparator();
134
135        // Technology section (alphabetical)
136        JMenu tech = new JMenu("Technology");
137        JMenuItem brailleNote = new JMenuItem(new AbstractAction("BrailleNote Touch") {
138            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillenote", null); }
139        });
140        brailleNote.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
141        brailleNote.setIcon(makeIcon(new Color(0x7B61FF), 12));
142        brailleNote.getAccessibleContext().setAccessibleName("BrailleNote Touch");
143        brailleNote.getAccessibleContext().setAccessibleDescription("Open the BrailleNote Touch page");
144        tech.add(brailleNote);
145
146        JMenuItem brailleSense = new JMenuItem(new AbstractAction("Braille Sense") {
147            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillesense", null); }
148        });
149        brailleSense.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
150        brailleSense.setIcon(makeIcon(new Color(0xF8E71C), 12));
151        brailleSense.getAccessibleContext().setAccessibleName("Braille Sense");
152        brailleSense.getAccessibleContext().setAccessibleDescription("Open the Braille Sense page");
153        tech.add(brailleSense);
154
155        JMenuItem dl = new JMenuItem(new AbstractAction("Digital Literacy") {
156            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("digitalliteracy", null); }
157        });
158        dl.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
159        dl.setIcon(makeIcon(new Color(0x7ED321), 12));
160        dl.getAccessibleContext().setAccessibleName("Digital Literacy");
161        dl.getAccessibleContext().setAccessibleDescription("Open the Digital Literacy page");
162        tech.add(dl);
163
164        JMenuItem ios = new JMenuItem(new AbstractAction("iOS") {
165            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("ios", null); }
166        });
167        ios.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
168        ios.setIcon(makeIcon(new Color(0x00A5E0), 12));
169        ios.getAccessibleContext().setAccessibleName("iOS");
170        ios.getAccessibleContext().setAccessibleDescription("Open the iOS accessibility page");
171        tech.add(ios);
172
173        JMenuItem keyboarding = new JMenuItem(new AbstractAction("Keyboarding") {
174            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("keyboarding", null); }
175        });
176        keyboarding.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
177        keyboarding.setIcon(makeIcon(new Color(0x8B572A), 12));
178        keyboarding.getAccessibleContext().setAccessibleName("Keyboarding");
179        keyboarding.getAccessibleContext().setAccessibleDescription("Open the Keyboarding skills page");
180        tech.add(keyboarding);
181
182        JMenuItem screenReader = new JMenuItem(new AbstractAction("Screen Reader") {
183            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("screenreader", null); }
184        });
185        screenReader.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
186        screenReader.setIcon(makeIcon(new Color(0x417505), 12));
187        screenReader.getAccessibleContext().setAccessibleName("Screen Reader");
188        screenReader.getAccessibleContext().setAccessibleDescription("Open the Screen Reader page");
189        tech.add(screenReader);
190
191        nav.add(tech);
192        nav.addSeparator();
193
194        // Misc (alphabetical)
195        JMenu misc = new JMenu("Misc");
196        JMenuItem contactLog = new JMenuItem(new AbstractAction("Contact Log") {
197            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("contactlog", null); }
198        });
199        contactLog.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
200        contactLog.setIcon(makeIcon(new Color(0xF18805), 12));
201        contactLog.getAccessibleContext().setAccessibleName("Contact Log");
202        contactLog.getAccessibleContext().setAccessibleDescription("Open the Contact Log page");
203        misc.add(contactLog);
204
205        JMenuItem observations = new JMenuItem(new AbstractAction("Observations") {
206            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("observations", null); }
207        });
208        observations.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
209        observations.setIcon(makeIcon(new Color(0x50E3C2), 12));
210        observations.getAccessibleContext().setAccessibleName("Observations");
211        observations.getAccessibleContext().setAccessibleDescription("Open the Observations page");
212        misc.add(observations);
213
214        JMenuItem sessionNotes = new JMenuItem(new AbstractAction("Session Notes") {
215            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("sessionnotes", null); }
216        });
217        sessionNotes.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
218        sessionNotes.setIcon(makeIcon(new Color(0xD0021B), 12));
219        sessionNotes.getAccessibleContext().setAccessibleName("Session Notes");
220        sessionNotes.getAccessibleContext().setAccessibleDescription("Open the Session Notes page");
221        misc.add(sessionNotes);
222
223        nav.add(misc);
224
225        mb.add(nav);
226
227        // Themes menu (top-level)
228        JMenu themesMenu = new JMenu("Themes");
229        // Read persisted theme choice so we can mark the active menu item
230        String currentTheme = com.studentgui.apphelpers.Settings.get("theme", "light");
231
232        javax.swing.ButtonGroup themeGroup = new javax.swing.ButtonGroup();
233
234        javax.swing.JRadioButtonMenuItem light = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Light") {
235            @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("light"); com.studentgui.apphelpers.Settings.put("theme", "light"); }
236        });
237        light.setIcon(makeIcon(new Color(0x000000), 12));
238        light.getAccessibleContext().setAccessibleName("Light theme");
239        light.getAccessibleContext().setAccessibleDescription("Switch to the light theme");
240        if ("light".equalsIgnoreCase(currentTheme) || "flatlightlaf".equalsIgnoreCase(currentTheme)) {
241            light.setSelected(true);
242        }
243        themeGroup.add(light);
244        themesMenu.add(light);
245
246        javax.swing.JRadioButtonMenuItem dark = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Dark") {
247            @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("dark"); com.studentgui.apphelpers.Settings.put("theme", "dark"); }
248        });
249        dark.setIcon(makeIcon(new Color(0x2C2C2C), 12));
250        dark.getAccessibleContext().setAccessibleName("Dark theme");
251        dark.getAccessibleContext().setAccessibleDescription("Switch to the dark theme");
252        if ("dark".equalsIgnoreCase(currentTheme) || "flatdarklaf".equalsIgnoreCase(currentTheme)) {
253            dark.setSelected(true);
254        }
255        themeGroup.add(dark);
256        themesMenu.add(dark);
257
258        javax.swing.JRadioButtonMenuItem intellij = new javax.swing.JRadioButtonMenuItem(new AbstractAction("IntelliJ (Darcula)") {
259            @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("darcula"); com.studentgui.apphelpers.Settings.put("theme", "darcula"); }
260        });
261        intellij.setIcon(makeIcon(new Color(0x4A4A4A), 12));
262        intellij.getAccessibleContext().setAccessibleName("IntelliJ Darcula");
263        intellij.getAccessibleContext().setAccessibleDescription("Switch to the IntelliJ Darcula theme");
264        if ("darcula".equalsIgnoreCase(currentTheme)) {
265            intellij.setSelected(true);
266        }
267        themeGroup.add(intellij);
268        themesMenu.add(intellij);
269        themesMenu.addSeparator();
270
271        // Dynamically add all IntelliJ themes available from flatlaf-intellij-themes
272        // Discover and add IntelliJ themes from the flatlaf-intellij-themes artifact if present
273        List<String> intellijThemes = listClassesInPackage("com.formdev.flatlaf.intellijthemes");
274        if (!intellijThemes.isEmpty()) {
275            JMenu intellijGroup = new JMenu("IntelliJ Themes");
276            for (String cls : intellijThemes) {
277                final String className = cls;
278                    JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) {
279                    @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); }
280                });
281                mi.setIcon(makeIcon(new Color(0x888888), 10));
282                mi.getAccessibleContext().setAccessibleName(className);
283                mi.getAccessibleContext().setAccessibleDescription("Apply " + className);
284                intellijGroup.add(mi);
285            }
286            themesMenu.add(intellijGroup);
287        }
288
289        // Material themes: if user adds flatlaf-themes or material themes library, we can try to load them by class name
290        JMenu materialGroup = new JMenu("Material Themes");
291        List<String> materialThemes = listClassesInPackage("com.formdev.flatlaf.materialthemes");
292        for (String cls : materialThemes) {
293            final String className = cls;
294            JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) {
295                @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); }
296            });
297            mi.setIcon(makeIcon(new Color(0x666666), 10));
298            mi.getAccessibleContext().setAccessibleName(className);
299            mi.getAccessibleContext().setAccessibleDescription("Apply " + className);
300            materialGroup.add(mi);
301        }
302        themesMenu.add(materialGroup);
303
304        // Material Theme UI Lite (popular collection) - add specific known classes when available
305        JMenu materialLiteGroup = new JMenu("Material Theme UI Lite");
306        String[][] lite = new String[][]{
307            {"Arc Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkIJTheme"},
308            {"Arc Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkContrastIJTheme"},
309            {"Atom One Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkIJTheme"},
310            {"Atom One Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkContrastIJTheme"},
311            {"Atom One Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightIJTheme"},
312            {"Atom One Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightContrastIJTheme"},
313            {"Dracula (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaIJTheme"},
314            {"Dracula Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaContrastIJTheme"},
315            {"GitHub (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubIJTheme"},
316            {"GitHub Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubContrastIJTheme"},
317            {"GitHub Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkIJTheme"},
318            {"GitHub Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkContrastIJTheme"},
319            {"Light Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlIJTheme"},
320            {"Light Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlContrastIJTheme"},
321            {"Material Darker (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme"},
322            {"Material Darker Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerContrastIJTheme"},
323            {"Material Deep Ocean (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanIJTheme"},
324            {"Material Deep Ocean Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanContrastIJTheme"},
325            {"Material Lighter (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterIJTheme"},
326            {"Material Lighter Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterContrastIJTheme"},
327            {"Material Oceanic (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicIJTheme"},
328            {"Material Oceanic Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicContrastIJTheme"},
329            {"Material Palenight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightIJTheme"},
330            {"Material Palenight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightContrastIJTheme"},
331            {"Monokai Pro (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProIJTheme"},
332            {"Monokai Pro Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProContrastIJTheme"},
333            {"Moonlight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightIJTheme"},
334            {"Moonlight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightContrastIJTheme"},
335            {"Night Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlIJTheme"},
336            {"Night Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlContrastIJTheme"},
337            {"Solarized Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkIJTheme"},
338            {"Solarized Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkContrastIJTheme"},
339            {"Solarized Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightIJTheme"},
340            {"Solarized Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightContrastIJTheme"}
341        };
342        for (String[] entry : lite) {
343            final String label = entry[0];
344            final String className = entry[1];
345            try {
346                Class.forName(className);
347                JMenuItem mi = new JMenuItem(new AbstractAction(label) {
348                    @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); }
349                });
350                mi.setIcon(makeIcon(new Color(0x666666), 10));
351                mi.getAccessibleContext().setAccessibleName(className);
352                mi.getAccessibleContext().setAccessibleDescription("Apply " + className);
353                materialLiteGroup.add(mi);
354            } catch (ClassNotFoundException cnfe) {
355                // class not on classpath - ignore
356            }
357        }
358        // Only add the group if at least one theme was found
359        if (materialLiteGroup.getMenuComponentCount() > 0) {
360            themesMenu.add(materialLiteGroup);
361        }
362
363        mb.add(themesMenu);
364        return mb;
365    }
366
367    /**
368     * Private constructor to prevent instantiation of this utility class.
369     */
370    private Theme() {
371        throw new AssertionError("Not instantiable");
372    }
373
374    /**
375     * Create a small square color icon used for menu items. Kept local to avoid
376     * needing external resources; a simple filled rounded rectangle is drawn.
377     */
378    private static ImageIcon makeIcon(final Color color, final int size) {
379        BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
380        Graphics2D g = img.createGraphics();
381        try {
382            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
383            g.setColor(color);
384            g.fillRoundRect(0, 0, size, size, Math.max(2, size/4), Math.max(2, size/4));
385        } finally {
386            g.dispose();
387        }
388        return new ImageIcon(img);
389    }
390
391    // Return the simple class name from a fully-qualified class name
392    private static String simpleName(final String fqcn) {
393        int idx = fqcn.lastIndexOf('.');
394        return idx >= 0 ? fqcn.substring(idx + 1) : fqcn;
395    }
396
397    // List classes in a package by scanning classpath entries. This is a best-effort
398    // method: it handles classes inside jars and on the filesystem.
399    private static List<String> listClassesInPackage(final String packageName) {
400        List<String> results = new ArrayList<>();
401        String path = packageName.replace('.', '/');
402        try {
403            ClassLoader cl = Thread.currentThread().getContextClassLoader();
404            Enumeration<URL> resources = cl.getResources(path);
405            while (resources.hasMoreElements()) {
406                URL url = resources.nextElement();
407                URLConnection conn = url.openConnection();
408                if (conn instanceof JarURLConnection) {
409                    JarURLConnection juc = (JarURLConnection) conn;
410                    try (JarFile jar = juc.getJarFile()) {
411                        Enumeration<JarEntry> entries = jar.entries();
412                        while (entries.hasMoreElements()) {
413                            JarEntry je = entries.nextElement();
414                            String name = je.getName();
415                            if (name.startsWith(path) && name.endsWith(".class") && !je.isDirectory()) {
416                                String cls = name.replace('/', '.').replaceAll("\\.class$", "");
417                                results.add(cls);
418                            }
419                        }
420                    }
421                } else {
422                    try {
423                        URI uri = url.toURI();
424                        File folder = new File(uri);
425                        if (folder.isDirectory()) {
426                            File[] files = folder.listFiles();
427                            if (files != null) {
428                                for (File f : files) {
429                                    if (f.isFile() && f.getName().endsWith(".class")) {
430                                        String cls = packageName + "." + f.getName().replaceAll("\\.class$", "");
431                                        results.add(cls);
432                                    }
433                                }
434                            }
435                        }
436                    } catch (java.net.URISyntaxException ioe) {
437                        // ignore
438                    }
439                }
440            }
441        } catch (IOException e) {
442            // ignore
443        }
444        return results;
445    }
446}