public class MenuScroller {

  private JPopupMenu menu;
  private Component[] menuItems;
  private MenuScrollItem upItem;
  private MenuScrollItem downItem;
  private final MenuScrollListener menuListener = new MenuScrollListener();
  private int scrollCount;
  private int interval;
  private int topFixedCount;
  private int bottomFixedCount;
  private int firstIndex = 0;
  private int keepVisibleIndex = -1;

  /**
   * Registers a menu to be scrolled with the default number of items to
   * display at a time and the default scrolling interval.
   * 
   * @param menu the menu
   * @return the MenuScroller
   */
  public static MenuScroller setScrollerFor(JMenu menu) {
    return new MenuScroller(menu);
  }

  /**
   * Registers a popup menu to be scrolled with the default number of items to
   * display at a time and the default scrolling interval.
   * 
   * @param menu the popup menu
   * @return the MenuScroller
   */
  public static MenuScroller setScrollerFor(JPopupMenu menu) {
    return new MenuScroller(menu);
  }

  /**
   * Registers a menu to be scrolled with the default number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display at a time
   * @return the MenuScroller
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) {
    return new MenuScroller(menu, scrollCount);
  }

  /**
   * Registers a popup menu to be scrolled with the default number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display at a time
   * @return the MenuScroller
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) {
    return new MenuScroller(menu, scrollCount);
  }

  /**
   * Registers a menu to be scrolled, with the specified number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to be displayed at a time
   * @param interval the scroll interval, in milliseconds
   * @return the MenuScroller
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) {
    return new MenuScroller(menu, scrollCount, interval);
  }

  /**
   * Registers a popup menu to be scrolled, with the specified number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to be displayed at a time
   * @param interval the scroll interval, in milliseconds
   * @return the MenuScroller
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) {
    return new MenuScroller(menu, scrollCount, interval);
  }

  /**
   * Registers a menu to be scrolled, with the specified number of items
   * to display in the scrolling region, the specified scrolling interval,
   * and the specified numbers of items fixed at the top and bottom of the
   * menu.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0.
   * @param bottomFixedCount the number of items to fix at the bottom. May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   * @return the MenuScroller
   */
  public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    return new MenuScroller(menu, scrollCount, interval,
            topFixedCount, bottomFixedCount);
  }

  /**
   * Registers a popup menu to be scrolled, with the specified number of items
   * to display in the scrolling region, the specified scrolling interval,
   * and the specified numbers of items fixed at the top and bottom of the
   * popup menu.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0
   * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   * @return the MenuScroller
   */
  public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    return new MenuScroller(menu, scrollCount, interval,
            topFixedCount, bottomFixedCount);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a menu with the
   * default number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the menu
   */
  public MenuScroller(JMenu menu) {
    this(menu, 15);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
   * default number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the popup menu
   */
  public MenuScroller(JPopupMenu menu) {
    this(menu, 15);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a menu with the
   * specified number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display at a time
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public MenuScroller(JMenu menu, int scrollCount) {
    this(menu, scrollCount, 150);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
   * specified number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display at a time
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public MenuScroller(JPopupMenu menu, int scrollCount) {
    this(menu, scrollCount, 150);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a menu with the
   * specified number of items to display at a time, and specified scrolling
   * interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display at a time
   * @param interval the scroll interval, in milliseconds
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public MenuScroller(JMenu menu, int scrollCount, int interval) {
    this(menu, scrollCount, interval, 0, 0);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
   * specified number of items to display at a time, and specified scrolling
   * interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display at a time
   * @param interval the scroll interval, in milliseconds
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public MenuScroller(JPopupMenu menu, int scrollCount, int interval) {
    this(menu, scrollCount, interval, 0, 0);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a menu with the
   * specified number of items to display in the scrolling region, the
   * specified scrolling interval, and the specified numbers of items fixed at
   * the top and bottom of the menu.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0
   * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   */
  public MenuScroller(JMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount);
  }

  /**
   * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
   * specified number of items to display in the scrolling region, the
   * specified scrolling interval, and the specified numbers of items fixed at
   * the top and bottom of the popup menu.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0
   * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   */
  public MenuScroller(JPopupMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    if (scrollCount <= 0 || interval <= 0) {
      throw new IllegalArgumentException("scrollCount and interval must be greater than 0");
    }
    if (topFixedCount < 0 || bottomFixedCount < 0) {
      throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative");
    }

    upItem = new MenuScrollItem(MenuIcon.UP, -1);
    downItem = new MenuScrollItem(MenuIcon.DOWN, +1);
    setScrollCount(scrollCount);
    setInterval(interval);
    setTopFixedCount(topFixedCount);
    setBottomFixedCount(bottomFixedCount);

    this.menu = menu;
    menu.addPopupMenuListener(menuListener);
  }

  /**
   * Returns the scroll interval in milliseconds
   * 
   * @return the scroll interval in milliseconds
   */
  public int getInterval() {
    return interval;
  }

  /**
   * Sets the scroll interval in milliseconds
   * 
   * @param interval the scroll interval in milliseconds
   * @throws IllegalArgumentException if interval is 0 or negative
   */
  public void setInterval(int interval) {
    if (interval <= 0) {
      throw new IllegalArgumentException("interval must be greater than 0");
    }
    upItem.setInterval(interval);
    downItem.setInterval(interval);
    this.interval = interval;
  }

  /**
   * Returns the number of items in the scrolling portion of the menu.
   *
   * @return the number of items to display at a time
   */
  public int getscrollCount() {
    return scrollCount;
  }

  /**
   * Sets the number of items in the scrolling portion of the menu.
   * 
   * @param scrollCount the number of items to display at a time
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public void setScrollCount(int scrollCount) {
    if (scrollCount <= 0) {
      throw new IllegalArgumentException("scrollCount must be greater than 0");
    }
    this.scrollCount = scrollCount;
    MenuSelectionManager.defaultManager().clearSelectedPath();
  }

  /**
   * Returns the number of items fixed at the top of the menu or popup menu.
   * 
   * @return the number of items
   */
  public int getTopFixedCount() {
    return topFixedCount;
  }

  /**
   * Sets the number of items to fix at the top of the menu or popup menu.
   * 
   * @param topFixedCount the number of items
   */
  public void setTopFixedCount(int topFixedCount) {
    if (firstIndex <= topFixedCount) {
      firstIndex = topFixedCount;
    } else {
      firstIndex += (topFixedCount - this.topFixedCount);
    }
    this.topFixedCount = topFixedCount;
  }

  /**
   * Returns the number of items fixed at the bottom of the menu or popup menu.
   * 
   * @return the number of items
   */
  public int getBottomFixedCount() {
    return bottomFixedCount;
  }

  /**
   * Sets the number of items to fix at the bottom of the menu or popup menu.
   * 
   * @param bottomFixedCount the number of items
   */
  public void setBottomFixedCount(int bottomFixedCount) {
    this.bottomFixedCount = bottomFixedCount;
  }

  /**
   * Scrolls the specified item into view each time the menu is opened.  Call this method with
   * <code>null</code> to restore the default behavior, which is to show the menu as it last
   * appeared.
   *
   * @param item the item to keep visible
   * @see #keepVisible(int)
   */
  public void keepVisible(JMenuItem item) {
    if (item == null) {
      keepVisibleIndex = -1;
    } else {
      int index = menu.getComponentIndex(item);
      keepVisibleIndex = index;
    }
  }

  /**
   * Scrolls the item at the specified index into view each time the menu is opened.  Call this
   * method with <code>-1</code> to restore the default behavior, which is to show the menu as
   * it last appeared.
   *
   * @param index the index of the item to keep visible
   * @see #keepVisible(javax.swing.JMenuItem)
   */
  public void keepVisible(int index) {
    keepVisibleIndex = index;
  }

  /**
   * Removes this MenuScroller from the associated menu and restores the
   * default behavior of the menu.
   */
  public void dispose() {
    if (menu != null) {
      menu.removePopupMenuListener(menuListener);
      menu = null;
    }
  }

  /**
   * Ensures that the <code>dispose</code> method of this MenuScroller is
   * called when there are no more refrences to it.
   * 
   * @exception  Throwable if an error occurs.
   * @see MenuScroller#dispose()
   */
  @Deprecated
  protected void finalize() throws Throwable {
    dispose();
  }

  private void refreshMenu() {
    if (menuItems != null && menuItems.length > 0) {
      firstIndex = Math.max(topFixedCount, firstIndex);
      firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex);

      upItem.setEnabled(firstIndex > topFixedCount);
      downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount);

      menu.removeAll();
      for (int i = 0; i < topFixedCount; i++) {
        menu.add(menuItems[i]);
      }
      if (topFixedCount > 0) {
        menu.addSeparator();
      }

      menu.add(upItem);
      for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
        menu.add(menuItems[i]);
      }
      menu.add(downItem);

      if (bottomFixedCount > 0) {
        menu.addSeparator();
      }
      for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) {
        menu.add(menuItems[i]);
      }

      JComponent parent = (JComponent) upItem.getParent();
      parent.revalidate();
      parent.repaint();
    }
  }

  private class MenuScrollListener implements PopupMenuListener {

    @Override
    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
      setMenuItems();
    }

    @Override
    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
      restoreMenuItems();
    }

    @Override
    public void popupMenuCanceled(PopupMenuEvent e) {
      restoreMenuItems();
    }

    private void setMenuItems() {
      menuItems = menu.getComponents();
      if (keepVisibleIndex >= topFixedCount
              && keepVisibleIndex <= menuItems.length - bottomFixedCount
              && (keepVisibleIndex > firstIndex + scrollCount
              || keepVisibleIndex < firstIndex)) {
        firstIndex = Math.min(firstIndex, keepVisibleIndex);
        firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1);
      }
      if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) {
        refreshMenu();
      }
    }

    private void restoreMenuItems() {
      menu.removeAll();
      for (Component component : menuItems) {
        menu.add(component);
      }
    }
  }

  private class MenuScrollTimer extends Timer {

    public MenuScrollTimer(final int increment, int interval) {
      super(interval, new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
          firstIndex += increment;
          refreshMenu();
        }
      });
    }
  }

  private class MenuScrollItem extends JMenuItem
          implements ChangeListener {

    private MenuScrollTimer timer;

    public MenuScrollItem(MenuIcon icon, int increment) {
      setIcon(icon);
      setDisabledIcon(icon);
      timer = new MenuScrollTimer(increment, interval);
      addChangeListener(this);
    }

    public void setInterval(int interval) {
      timer.setDelay(interval);
    }

    @Override
    public void stateChanged(ChangeEvent e) {
      if (isArmed() && !timer.isRunning()) {
        timer.start();
      }
      if (!isArmed() && timer.isRunning()) {
        timer.stop();
      }
    }
  }

  private enum MenuIcon implements Icon {

    UP(9, 1, 9),
    DOWN(1, 9, 1);
    final int[] xPoints = {1, 5, 9};
    final int[] yPoints;

    MenuIcon(int... yPoints) {
      this.yPoints = yPoints;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
      Dimension size = c.getSize();
      Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
      g2.setColor(Color.GRAY);
      g2.drawPolygon(xPoints, yPoints, 3);
      if (c.isEnabled()) {
        g2.setColor(Color.BLACK);
        g2.fillPolygon(xPoints, yPoints, 3);
      }
      g2.dispose();
    }

    @Override
    public int getIconWidth() {
      return 0;
    }

    @Override
    public int getIconHeight() {
      return 10;
    }
  }
} public class Config {

    private static final String KEY_WAR_PATH = "war-path";

    private static final String KEY_THEME = "theme";

    private static final String KEY_LANGUAGE = "language";

    private static final String KEY_WINDOW_LOCATION_X = "window-x";

    private static final String KEY_WINDOW_LOCATION_Y = "window-y";

    private static final String KEY_WINDOW_WIDTH = "window-width";

    private static final String KEY_WINDOW_HEIGHT = "window-height";

    private static final String KEY_PROGRESS_LOCATION_X = "progress-x";

    private static final String KEY_PROGRESS_LOCATION_Y = "progress-y";

    private static final String KEY_PROGRESS_WIDTH = "progress-width";

    private static final String KEY_PROGRESS_HEIGHT = "progress-height";

    private static final String KEY_AUTO_CLOSE = "auto-close";

    private static final String KEY_PREFIX_ADDON_UPLOAD = "addon-upload-";

    private static final String LOG_FILE = "waraddonclient.log";

    private static final String LINUX_LOG_FILE = "/var/log/waraddonclient.log";

    private static final String ADDON_FOLDER = "/Interface/AddOns/";

    private static final String LOGS = "/logs/";

    private static final String BASE_URL = "https://tools.idrinth.de/";

    private static final String VERSION_FILE = "/self.idrinth";
    
    private static final String AUTO_BACKUP_ON_UPDATE_ALL = "auto-update.on.update-all";

    private final Preferences prefs =  Preferences.userNodeForPackage(Main.class);

    private final String version;

    private final File logFile;

    private final File jarDir;

    public Config() throws IOException, URISyntaxException {
        //check to look for old conf
        if (prefs.get(KEY_THEME, "missing").equals("missing")) {
            Preferences oldPref = Preferences.userRoot().node(Config.class.getCanonicalName());
            prefs.put(KEY_THEME, oldPref.get(KEY_THEME, getTheme()));
            prefs.put(KEY_WAR_PATH, oldPref.get(KEY_WAR_PATH, getWARPath()));
            prefs.put(KEY_LANGUAGE, oldPref.get(KEY_LANGUAGE, getLanguage()));
            prefs.putInt(KEY_WINDOW_HEIGHT, oldPref.getInt(KEY_WINDOW_HEIGHT, getWindowDimension().height));
            prefs.putInt(KEY_WINDOW_WIDTH, oldPref.getInt(KEY_WINDOW_WIDTH, getWindowDimension().width));
            prefs.putInt(KEY_WINDOW_LOCATION_X, oldPref.getInt(KEY_WINDOW_LOCATION_X, getWindowPosition().x));
            prefs.putInt(KEY_WINDOW_LOCATION_Y, oldPref.getInt(KEY_WINDOW_LOCATION_Y, getWindowPosition().y));
            //cleanup and remove old preferences as we have now extracted all info from it
            try {
                oldPref.removeNode();
                oldPref.flush();
            } catch (BackingStoreException e) {
                //do nothing
            }
        }
        version = IOUtils.toString(Objects.requireNonNull(Config.class.getResourceAsStream("/version")), StandardCharsets.UTF_8);
        jarDir = new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParentFile();
        if (System.getProperty("os.name").startsWith("Windows")) {
            logFile = new File(jarDir.getAbsolutePath(), LOG_FILE);
        } else {
            logFile = new File(LINUX_LOG_FILE);
        }
    }

    public void setWARPath(String path) {
        prefs.put(KEY_WAR_PATH, path);
    }

    public String getWARPath() {
        return prefs.get(KEY_WAR_PATH, ".");
    }

    public String getTheme() {
        return prefs.get(KEY_THEME, "Nimbus");
    }

    public void setTheme(String theme) {
        prefs.put(KEY_THEME, theme);
    }

    public String getLanguage() {
        return prefs.get(KEY_LANGUAGE, "en");
    }

    public void setLanguage(String language) {
        prefs.put(KEY_LANGUAGE, language);
    }

    public int getAutoClose() {
        return prefs.getInt(KEY_AUTO_CLOSE, 60);
    }

    public void setAutoClose(int autoClose) {
        prefs.putInt(KEY_AUTO_CLOSE, autoClose);
    }

    public void setWindowDimension(Dimension dimension) {
        prefs.putInt(KEY_WINDOW_WIDTH, dimension.width);
        prefs.putInt(KEY_WINDOW_HEIGHT, dimension.height);
    }

    public Dimension getWindowDimension() {
        return new Dimension(
            prefs.getInt(KEY_WINDOW_WIDTH, 800),
            prefs.getInt(KEY_WINDOW_HEIGHT, 450)
        );
    }

    public void setWindowPosition(Point point) {
        prefs.putInt(KEY_WINDOW_LOCATION_X, point.x);
        prefs.putInt(KEY_WINDOW_LOCATION_Y, point.y);
    }

    public Point getWindowPosition() {
        return new Point(
            prefs.getInt(KEY_WINDOW_LOCATION_X, 0),
            prefs.getInt(KEY_WINDOW_LOCATION_Y, 0)
        );
    }

    public void setProgressDimension(Dimension dimension) {
        prefs.putInt(KEY_PROGRESS_WIDTH, dimension.width);
        prefs.putInt(KEY_PROGRESS_HEIGHT, dimension.height);
    }

    public Dimension getProgressDimension() {
        return new Dimension(
            prefs.getInt(KEY_PROGRESS_WIDTH, 300),
            prefs.getInt(KEY_PROGRESS_HEIGHT, 150)
        );
    }

    public void setProgressPosition(Point point) {
        prefs.putInt(KEY_PROGRESS_LOCATION_X, point.x);
        prefs.putInt(KEY_PROGRESS_LOCATION_Y, point.y);
    }

    public Point getProgressPosition() {
        return new Point(
            prefs.getInt(KEY_PROGRESS_LOCATION_X, 0),
            prefs.getInt(KEY_PROGRESS_LOCATION_Y, 0)
        );
    }

    public String getVersion() {
        return version;
    }

    public boolean isEnabled(String addon) {
        return prefs.getBoolean(KEY_PREFIX_ADDON_UPLOAD + addon, false);
    }

    public void setEnabled(String addon, boolean enable) {
        prefs.putBoolean(KEY_PREFIX_ADDON_UPLOAD + addon, enable);
    }

    public File getLogFile() {
        return logFile;
    }

    public String getAddonFolder() {
        return getWARPath() + ADDON_FOLDER;
    }

    public String getLogsFolder() {
        return getWARPath() + LOGS;
    }

    public String getURL() {
        return BASE_URL;
    }
    
    public File getJarDir() {
        return jarDir;
    }

    public String getVersionFile() {
        return VERSION_FILE;
    }

    public int getAutoBackupOnUpdateAll() {
        return prefs.getInt(AUTO_BACKUP_ON_UPDATE_ALL, 2);
    }

    public void setAutoBackupOnUpdateAll(int choice) {
        prefs.putInt(AUTO_BACKUP_ON_UPDATE_ALL, choice);
    }
}
 public interface Addon {

    List<String> getTags();

    boolean hasTag(String tag);

    String getVersion();

    String getInstalled();

    String getDescription(String language);

    String getName();

    String getStatus();

    public void install(ProgressReporter progress);

    public void uninstall(ProgressReporter progress);

    String getFile();

    String getReason();

    String getUrl();
    
    boolean showSettings();
    
    Object[] getTableRow();

    int getEndorsements();

    int getDownloads();
}
 public class GuiAddonList extends AddonList
{
    private ActionListener listener;
    
    private DefaultTableModel model;

    private JMenu menu;

    private final HashMap<String, Tag> tags = new HashMap<>();

    private final ArrayList<String> tagNames = new ArrayList<>();

    private final HashMap<String, WatchedFile> watchedFilesMap = new HashMap<>();
    
    private final ArrayList<Object[]> rowsToAdd = new ArrayList<>();
    
    public GuiAddonList(Request client, BaseLogger logger, XmlParser parser, Config config) {
        super(client, logger, parser, config);
    }

    public void setMenu(JMenu menu, ActionListener listener) {
        this.menu = menu;
        this.listener = listener;
    }

    public void setModel(DefaultTableModel model) {
        this.model = model;
        for (Object[] row: rowsToAdd) {
            model.addRow(row);
        }
        rowsToAdd.clear();
    }
    /**
     * processes the addon lst to see what addon is tagged with a specific tag
     */
    private void processAddons() {
        for (Addon addon : rows) {
            for (String tag : addon.getTags()) {
                if (!tags.containsKey(tag)) {
                    tags.put(tag, new Tag(tag, listener));
                }
                tags.get(tag).addMember(addon);
                if (!tagNames.contains(tag)) {
                    tagNames.add(tag);
                }
            }
        }
    }
    
    @Override
    public void add(Addon addon) {
        super.add(addon);
        if (model == null) {
            rowsToAdd.add(addon.getTableRow());
            return;
        }
        model.addRow(addon.getTableRow());
    }

    public List<String> getActiveTags() {
        ArrayList<String> active = new ArrayList<>();
        tagNames.stream().filter(tag -> (tags.get(tag).isActive())).forEach(active::add);
        return active;
    }

    /**
     * removes unneeded tags and adds new tags to the menu
     */
    private void processTags() {
        tagNames.stream().map(name -> {
            tags.get(name).checkMembers();
            return name;
        }).forEach(name -> {
            if (!tags.get(name).hasMembers()) {
                menu.remove(tags.get(name).getMenu());
                tags.remove(name);
                tagNames.remove(name);
            } else {
                menu.add(tags.get(name).getMenu());
            }
        });
    }

    public HashMap<String, WatchedFile> getWatchedFiles() {
        return watchedFilesMap;
    }

    @Override
    public void run() {
        processAddonDir();
        try {
            new JsonProcessor(client.getAddonList(), model).run();
        } catch (IOException exception) {
           logger.error(exception);
        }
        processAddons();
        processTags();
    }

    public class WatchedFile implements java.lang.Runnable {

        private boolean active;

        private File file;

        private final ArrayList<ActualAddon> list = new ArrayList<>();

        public void addAddon(ActualAddon addon) {
            list.add(addon);
        }

        public void setFileToProcess(File file2process) {
            while (active) {
                Utils.sleep(100, logger);
            }
            active = true;
            file = file2process;
            new java.lang.Thread(this).start();
        }

        @Override
        public void run() {
            if (file == null || !file.exists()) {
                active = false;
                return;
            }
            list.forEach(addon -> addon.fileWasChanged(file));
            active = false;
        }
    }
    
    public class JsonProcessor extends AddonList.JsonProcessor {
        
        private final DefaultTableModel model;

        public JsonProcessor(JsonArray parse, DefaultTableModel model) {
            super(parse);
            this.model = model;
        }

        @Override
        protected void newAddon(ActualAddon addon) {
            super.newAddon(addon);
            if (!addon.getFile().isEmpty()) {
                if (!watchedFilesMap.containsKey(addon.getFile().toLowerCase())) {
                    watchedFilesMap.put(addon.getFile().toLowerCase(), new WatchedFile());
                }
                watchedFilesMap.get(addon.getFile().toLowerCase()).addAddon(addon);
            }
        }

        @Override
        protected void existingAddon(ActualAddon addon) {
            super.existingAddon(addon);
            updateModel(addon);
        }

        @Override
        protected void unknownAddon(ActualAddon addon) {
            super.unknownAddon(addon);
            updateModel(addon);
        }
        
        private void updateModel(ActualAddon addon) {
            Object[] data = list.get(addon.getName()).getTableRow();
            int index = rows.indexOf(list.get(addon.getName()));
            model.setValueAt(data[0], index, 0);
            model.setValueAt(data[1], index, 1);
            model.setValueAt(data[2], index, 2);
            model.setValueAt(data[3], index, 3);
            model.setValueAt(data[4], index, 4);
            model.setValueAt(data[5], index, 5);
        }
    }
}
 public class ActualAddon implements Addon {

    private Map<String, String> descriptions = new HashMap<>();
    
    private boolean hasSettings = false;

    private String file = "";

    private String reason = "";

    private String url = "";

    private String version;

    private final String slug;

    private final String name;

    private int endorsements;

    private int downloads;

    private String installed = "-";

    private ArrayList<String> tags = new ArrayList<>();

    private final Request client;
    
    private final BaseLogger logger;
    
    private final XmlParser parser;
    
    private final Config config;
    
    private String defaultDescription = "";
    
    private boolean loadedDescription = false;
    
    private Executor runner;

    public ActualAddon(javax.json.JsonObject addon, Request client, BaseLogger logger, XmlParser parser, Config config, Executor runner) throws InvalidArgumentException {
        if (addon == null) {
            throw new InvalidArgumentException("Addon is null");
        }
        this.client = client;
        this.logger = logger;
        this.parser = parser;
        this.config = config;
        this.runner = runner;
        version = getStringFromObject("version", addon);
        slug = getStringFromObject("slug", addon);
        name = getStringFromObject("name", addon);
        try {
            downloads = Integer.parseInt(getStringFromObject("downloads", addon));
        } catch (NumberFormatException ex) {
            downloads = 0;
        }
        try {
            endorsements = Integer.parseInt(getStringFromObject("endorsements", addon));
        } catch (NumberFormatException ex) {
            endorsements = 0;
        }
        JsonArray tagList = addon.getJsonArray("tags");
        int counter = 0;
        while (tagList.size() > counter) {
            tags.add(tagList.getString(counter));
            counter++;
        }
        new VersionFinder(find(name)).run();
        refresh();
    }

    private File find(String name) {
        File folder = new File(config.getAddonFolder());
        if (folder.exists()) {
            for (File innerFolder : Objects.requireNonNull(folder.listFiles())) {
                if (innerFolder.getName().equalsIgnoreCase(name)) {
                    return innerFolder;
                }

            }
        }
        return new File(config.getAddonFolder() + name);
    }

    public ArrayList<String> getTags() {
        return tags;
    }

    /**
     * does this addon have the given tag?
     *
     * @param tag
     * @return boolean
     */
    public boolean hasTag(String tag) {
        return tags.stream().anyMatch(tag::equalsIgnoreCase);
    }

    /**
     * updates this instance with data from another instance
     *
     * @param addon
     */
    public void update(ActualAddon addon) {
        version = addon.getVersion();
        tags = addon.getTags();
        descriptions = addon.getDescriptions();
    }

    /**
     * the avaible version
     *
     * @return String
     */
    public String getVersion() {
        return version;
    }

    public String getInstalled() {
        return installed;
    }

    /**
     * try to find a string in a jsonObject
     *
     * @param key
     * @param data
     * @return String
     */
    private String getStringFromObject(String key, JsonObject data) {
        if (key != null && data != null && data.containsKey(key) && !data.isNull(key)) {
            return java.util.regex.Pattern.compile("(^\")|(\"$)").matcher(data.get(key).toString()).replaceAll("");
        }
        return "";
    }

    /**
     * get the table row configuration for this addon
     *
     * @return String[[
     */
    public Object[] getTableRow() {
        Object[] row = new Object[6];
        row[0] = getStatus();
        row[1] = name;
        row[2] = version;
        row[3] = installed;
        row[4] = endorsements;
        row[5] = downloads;
        return row;
    }

    public String getDescription(String language) {
        String description = "<p><strong>There is currently no Description for " + name + ".</strong></p>"
                + "<p>You can help by adding one at <a href=\"http://tools.idrinth.de/addons/" + slug
                + "/\">http://tools.idrinth.de/addons/" + slug + "/</a>.</p>"
                + "<p>"+ defaultDescription +"</p>";
        if (descriptions.containsKey(language) && !descriptions.get(language).isEmpty()) {
            description = descriptions.get(language);
        } else if (descriptions.get("en") != null &&  !descriptions.get("en").isEmpty()) {
            description = descriptions.get("en");
        }
        return "<html>" + description.replaceAll("(\\\\n|\\\\r)+", " ").replaceAll("\\\\\"", "\"");
    }

    public Runnable loadDescription(JEditorPane field, String language) {
        if (!descriptions.isEmpty() || loadedDescription) {
            return () -> {};
        }
        loadedDescription = true;
        return () -> {
            try {
                JsonObject addon = client.getAddon(slug);
                descriptions.put("en", getStringFromObject("description", addon));
                descriptions.put("de", getStringFromObject("description_de", addon));
                descriptions.put("fr", getStringFromObject("description_fr", addon));
                field.setText(getDescription(language));
            } catch (IOException ex) {
                loadedDescription = false;
                logger.error("Failed loading addon-data from server.");
            }
        };
    }

    /**
     * get a list of descriptions
     *
     * @return java.util.HashMap
     */
    public Map<String, String> getDescriptions() {
        return descriptions;
    }

    public String getName() {
        return name;
    }

    public void install(ProgressReporter reporter) {
        reporter.incrementMax(3);
        runner.execute(new Updater(reporter, find(name), true));
    }

    public void uninstall(ProgressReporter reporter) {
        reporter.incrementMax(2);
        runner.execute(new Updater(reporter, find(name), false));
    }

    public void fileWasChanged(File changedFile) {
        if (config.isEnabled(name) && changedFile.isFile() && changedFile.getName().equalsIgnoreCase(file)) {
            try {
                client.upload(url, changedFile);
            } catch (Exception exception) {
                logger.warn(exception);
            }
        }
    }

    @Override
    public String getStatus() {
        if("-".equals(getInstalled())) {
            return " ";
        }
        try {
            Version remote = Version.valueOf(getVersion());
            Version local = Version.valueOf(getInstalled());
            if (remote.equals(local)) {
                return "✓";
            }
            if (remote.lessThan(local)) {
                return "?";
            }
        } catch (com.github.zafarkhaja.semver.ParseException e) {
            return "X";
        }
        return "X";
    }

    @Override
    public int getEndorsements() {
        return endorsements;
    }

    @Override
    public int getDownloads() {
        return downloads;
    }

    private class Updater implements Runnable {
        private final boolean redeploy;
        private final File folder;
        private final ProgressReporter reporter;
        public Updater(ProgressReporter reporter, File folder, boolean redeploy) {
            this.reporter = reporter;
            this.folder = folder;
            this.redeploy = redeploy;
        }
        public void run() {
            try {
                uninstall(folder);
            } catch (IOException ex) {
                logger.error(ex);
            }
            reporter.incrementCurrent();
            if (redeploy) {
                try {
                    install();
                } catch (IOException ex) {
                    logger.error(ex);
                }
                reporter.incrementCurrent();
            }
            refresh();
            reporter.incrementCurrent();
            config.setEnabled(name, false);
        }

        private void uninstall(File addonFolder) throws IOException {
            try {
                File versionFile = new File(addonFolder.getPath() + config.getVersionFile());
                if (!versionFile.exists()) {
                    File zip = getZip();
                    writeMetaDataFile(new ZipFile(zip));
                    FileUtils.deleteQuietly(zip);
                }
                NodeList list = parser.parse(versionFile).getElementsByTagName("folder");
                if (list.getLength() == 0) {
                    File zip = getZip();
                    writeMetaDataFile(new ZipFile(zip));
                    FileUtils.deleteQuietly(zip);
                    list = parser.parse(versionFile).getElementsByTagName("folder");
                }
                for (int i=0;i<list.getLength();i++) {
                    Utils.deleteFolder(new File(config.getAddonFolder() + list.item(i).getTextContent()));
                }
            } catch (IOException | SAXException e) {
                logger.warn(e);
            }
            Utils.deleteFolder(addonFolder);
            installed="-";
        }

        private File getZip() throws IOException {
            File zip = new File(System.getProperty("java.io.tmpdir") + "/" + slug + ".zip");
            try (InputStream stream = client.getAddonDownload(slug + "/download/" + version.replace(".", "-") + "/")) {
                FileUtils.copyInputStreamToFile(stream, zip);
            }
            return zip;
        }

        private void install() throws IOException {
            File zip = getZip();
            try (ZipFile zipFile = new ZipFile(zip)) {
                zipFile.extractAll(config.getAddonFolder());
                writeMetaDataFile(zipFile);
            }
            FileUtils.deleteQuietly(zip);
            installed=version;
        }

        private void writeMetaDataFile(ZipFile zipFile) throws IOException {
            File tmp = new File(System.getProperty("java.io.tmpdir") + "/waraddonfolder/" + name);
            Utils.deleteFolder(tmp);
            tmp.mkdirs();
            zipFile.extractAll(tmp.getAbsolutePath());
            StringBuilder sb = new StringBuilder();
            ArrayList<String> folders = new ArrayList<>();
            for (File thisFolder : Objects.requireNonNull(tmp.listFiles())) {
                sb.append("<folder>");
                sb.append(thisFolder.getName());
                sb.append("</folder>");
                folders.add(thisFolder.getName());
            }
            Utils.deleteFolder(tmp);
            File target = find(name);
            folders.add(target.getName());
            target.mkdirs();
            for (String thisFolder : folders) {
                FileUtils.writeStringToFile(
                    new File(config.getAddonFolder() + thisFolder + config.getVersionFile()),
                    "<?xml version=\"1.0\" encoding=\"UTF-8\"?><UiMod>"
                    + "<name>" + name + "</name>"
                    + "<version>" + version + "</version>"
                    + "<folders>" + sb + "</folders>"
                    + "</UiMod>",
                    StandardCharsets.UTF_8
                );
            }
        }
    }

    private class VersionFinder {

        private final File folder;

        public VersionFinder(File folder) {
            this.folder = folder;
        }

        private boolean processDirectory() {
            for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
                if (!fileEntry.isDirectory()
                        && org.apache.commons.io.FilenameUtils.getExtension(fileEntry.getName()).equalsIgnoreCase("mod")) {
                    try {
                        Document doc = parser.parse(fileEntry, new SilencingErrorHandler(logger));
                        NodeList list = doc.getElementsByTagName("UiMod");
                        installed = list.item(0).getAttributes().getNamedItem("version").getTextContent();
                        NodeList description = doc.getElementsByTagName("Description");
                        if (description.getLength() > 0) {
                            defaultDescription = description.item(0).getAttributes().getNamedItem("text").getTextContent();
                        }
                        return true;
                    } catch (FactoryConfigurationError | SAXException | IOException exception) {
                        logger.warn(exception);
                    }
                }
            }
            return false;
        }

        /**
         * tries to find and set the default version
         */
        private boolean getDownloadVersion() {
            File versionFile = new File(folder.getPath() + config.getVersionFile());
            if (versionFile.exists()) {
                try {
                    NodeList list = parser.parse(versionFile).getElementsByTagName("version");
                    installed = list.item(0).getTextContent().replace("(sys)", "");
                    return true;
                } catch (FactoryConfigurationError | SAXException | IOException exception) {
                    logger.warn(exception);
                }
            }
            return false;
        }

        /**
         * starts searching for an installed version
         */
        public void run() {
            if (folder == null || !folder.exists()) {
                return;
            }
            if (!getDownloadVersion() && !processDirectory()) {
                installed = "-";
            }
        }
    }
    
    public boolean showSettings() {
        return hasSettings;
    }

    public final void refresh() {
        file = "";
        reason = "";
        url = "";
        hasSettings = false;
        File folder = find(name);
        if (folder.exists()) {
            for (File fileEntry : Objects.requireNonNull(folder.listFiles())) {
                processFile(fileEntry);
                if (hasSettings) {
                    return;
                }
            }
        }
    }

    private void processFile(File fileEntry) {
        if (fileEntry.isDirectory()) {
            return;
        }
        if (!"upload.idrinth".equalsIgnoreCase(fileEntry.getName())) {
            return;
        }
        try {
            NodeList list = parser.parse(fileEntry).getFirstChild().getChildNodes();
            for (int counter = 0; counter < list.getLength(); counter++) {
                processNode(list.item(counter));
            }
            hasSettings = true;
        } catch (FactoryConfigurationError | SAXException | IOException exception) {
            logger.error(exception);
        }
    }

    private void processNode(org.w3c.dom.Node item) {
        if ("file".equalsIgnoreCase(item.getNodeName())) {
            file = item.getTextContent();
            return;
        }
        if ("url".equalsIgnoreCase(item.getNodeName())) {
            url = item.getTextContent();
            return;
        }
        if ("reason".equalsIgnoreCase(item.getNodeName())) {
            reason = item.getTextContent();
        }
    }
    @Override
    public String getFile() {
        return file;
    }

    @Override
    public String getReason() {
        return reason;
    }

    @Override
    public String getUrl() {
        return url;
    }
} public class NoAddon implements de.idrinth.waraddonclient.model.addon.Addon {

    @Override
    public java.util.ArrayList<String> getTags() {
        return new java.util.ArrayList<>();
    }

    @Override
    public boolean hasTag(String tag) {
        return false;
    }

    @Override
    public String getVersion() {
        return "";
    }

    @Override
    public String getInstalled() {
        return "";
    }

    @Override
    public String getDescription(String language) {
        return "<html><h1>Welcome to the client.</h1><p>To get something more useful here, select an addon to the left.</p>";
    }

    @Override
    public String getName() {
        return "Welcome";
    }

    @Override
    public void install(ProgressReporter reporter) {
        throw new UnsupportedOperationException("You can not uninstall an addon that doesn't exist.");
    }

    @Override
    public void uninstall(ProgressReporter reporter) {
        throw new UnsupportedOperationException("You can not uninstall an addon that doesn't exist");
    }

    @Override
    public String getStatus() {
        return "unknown";
    }

    @Override
    public String getFile() {
        return "";
    }

    @Override
    public String getReason() {
        return "";
    }

    @Override
    public String getUrl() {
        return "";
    }

    @Override
    public boolean showSettings() {
        return false;
    }

    @Override
    public Object[] getTableRow() {
        Object[] row = new Object[6];
        row[0] = "";
        row[1] = "";
        row[2] = "";
        row[3] = "";
        row[4] = 0;
        row[5] = 0;
        return row;
    }

    @Override
    public int getEndorsements() {
        return 0;
    }

    @Override
    public int getDownloads() {
        return 0;
    }
}
 public class Backup {
    private final Config config;
    public Backup(Config config) {
        this.config = config;
    }

    public void create(ProgressReporter reporter) throws IOException {
        reporter.incrementMax(3);
        String warDir = config.getWARPath();
        java.io.File folder = new java.io.File(warDir+"/backups");
        if (!folder.isDirectory()) {
            folder.mkdir();
        }
        reporter.incrementCurrent();
        java.time.LocalDateTime now = java.time.LocalDateTime.now();
        File file = new java.io.File(warDir+"/backups/"+ now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm")) +".zip");
        try (net.lingala.zip4j.ZipFile zip = new net.lingala.zip4j.ZipFile(file)) {
            zip.addFolder(new File(warDir + "/user"));
            reporter.incrementCurrent();
            zip.addFolder(new File(warDir + "/Interface"));
        }
        reporter.incrementCurrent();
    }

    public void restore(java.io.File backup, ProgressReporter reporter) throws IOException {
        reporter.incrementMax(3);
        create(reporter);
        String warDir = config.getWARPath();
        try (net.lingala.zip4j.ZipFile zip = new net.lingala.zip4j.ZipFile(backup)) {
            reporter.incrementCurrent();
            Utils.emptyFolder(new File(warDir + "/Interface"));
            reporter.incrementCurrent();
            Utils.emptyFolder(new File(warDir + "/user"));
            reporter.incrementCurrent();
            zip.extractAll(warDir);
        }
        reporter.incrementCurrent();
    }
}
 public interface ProgressReporter
{
    void incrementCurrent();

    void incrementMax(int amount);

    void start(String title, Runnable callback);

    void stop();
}
 public abstract class BaseLogger {
    protected static final String LEVEL_INFO = "INFO";

    protected static final String LEVEL_ERROR = "ERROR";

    protected static final String LEVEL_WARNING = "WARN";

    protected abstract void log(String message, String severity);

    public void info(Throwable message) {
        info(message.getLocalizedMessage());
    }

    public void info(String message) {
        log(message, LEVEL_INFO);
    }

    public void warn(Throwable message) {
        warn(message.getLocalizedMessage());
    }

    public void warn(String message) {
        log(message, LEVEL_WARNING);
    }

    public void error(Throwable message) {
        error(message.getLocalizedMessage());
    }

    public void error(String message) {
        log(message, LEVEL_ERROR);
    }
}
 public class Shedule {
    private final List<ScheduledExecutorService> services = new ArrayList<>();
    public void register(int wait, Runnable run)
    {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        scheduler.scheduleAtFixedRate(run, 0, wait, SECONDS);
        services.add(scheduler);
    }
}

import darrylbu.util.MenuScroller;
import de.idrinth.waraddonclient.service.Config;
import de.idrinth.waraddonclient.model.addon.Addon;
import de.idrinth.waraddonclient.model.GuiAddonList;
import de.idrinth.waraddonclient.model.addon.ActualAddon;
import de.idrinth.waraddonclient.model.addon.NoAddon;
import de.idrinth.waraddonclient.service.Backup;
import de.idrinth.waraddonclient.service.ProgressReporter;
import javax.swing.table.TableRowSorter;
import de.idrinth.waraddonclient.service.logger.BaseLogger;
import de.idrinth.waraddonclient.service.Shedule;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.Serializable;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.LayoutStyle;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.WindowConstants;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;
package de.idrinth.waraddonclient.gui;


public class Addons extends BaseFrame implements MainWindow, Serializable {

    private static Addon activeAddon = new NoAddon();

    private final AtomicReference<GuiAddonList> addonList = new AtomicReference<>();

    private final AtomicReference<BaseLogger> logger = new AtomicReference<>();