gt:         if (items.size() != itemMap.size())
context: package me.desht.scrollingmenusign;
  
  import me.desht.dhutils.*;
  import me.desht.scrollingmenusign.enums.SMSAccessRights;
  import me.desht.scrollingmenusign.util.SMSUtil;
  import me.desht.scrollingmenusign.views.action.MenuDeleteAction;
  import me.desht.scrollingmenusign.views.action.RepaintAction;
  import me.desht.scrollingmenusign.views.action.TitleAction;
  import me.desht.scrollingmenusign.views.action.ViewUpdateAction;
  import org.apache.commons.lang.StringUtils;
  import org.bukkit.Bukkit;
  import org.bukkit.ChatColor;
  import org.bukkit.command.CommandSender;
  import org.bukkit.configuration.ConfigurationSection;
  import org.bukkit.configuration.MemoryConfiguration;
  import org.bukkit.entity.Player;
  import org.bukkit.plugin.Plugin;
  
  import java.io.File;
  import java.util.*;
  
  /**
   * Represents a menu object
   */
  public class SMSMenu extends Observable implements SMSPersistable, SMSUseLimitable, ConfigurationListener, Comparable<SMSMenu> {
      public static final String FAKE_SPACE = "\u203f";
  
      public static final String AUTOSORT = "autosort";
      public static final String DEFAULT_CMD = "defcmd";
      public static final String OWNER = "owner";
      public static final String TITLE = "title";
      public static final String ACCESS = "access";
      public static final String REPORT_USES = "report_uses";
      public static final String GROUP = "group";
  
      private final String name;
      private final List<SMSMenuItem> items = new ArrayList<SMSMenuItem>();
      private final Map<String, Integer> itemMap = new HashMap<String, Integer>();
      private final SMSRemainingUses uses;
      private final AttributeCollection attributes;    // menu attributes to be displayed and/or edited by players
  
      private String title;  // cache colour-parsed version of the title attribute
      private UUID ownerId;  // cache owner's UUID (could be null)
      private boolean autosave;
      private boolean inThaw;
  
      /**
       * Construct a new menu
       *
       * @param name  Name of the menu
       * @param title Title of the menu
       * @param owner Owner of the menu
       * @throws SMSException If there is already a menu at this location
       * @deprecated use {@link SMSMenu(String,String,Player)} or {@link SMSMenu(String,String, org.bukkit.plugin.Plugin )}
       */
      @Deprecated
      public SMSMenu(String name, String title, String owner) {
          this.name = name;
          this.uses = new SMSRemainingUses(this);
          this.attributes = new AttributeCollection(this);
          registerAttributes();
          setAttribute(OWNER, owner == null ? ScrollingMenuSign.CONSOLE_OWNER : owner);
          ownerId = ScrollingMenuSign.CONSOLE_UUID;
          setAttribute(TITLE, title);
          autosave = true;
      }
  
      /**
       * Construct a new menu
       *
       * @param name  Name of the menu
       * @param title Title of the menu
       * @param owner Owner of the menu
       * @throws SMSException If there is already a menu at this location
       */
      public SMSMenu(String name, String title, Player owner) {
          this.name = name;
          this.uses = new SMSRemainingUses(this);
          this.attributes = new AttributeCollection(this);
          registerAttributes();
          setAttribute(OWNER, owner == null ? ScrollingMenuSign.CONSOLE_OWNER : owner.getName());
          ownerId = owner == null ? ScrollingMenuSign.CONSOLE_UUID : owner.getUniqueId();
          setAttribute(TITLE, title);
          autosave = true;
      }
  
      /**
       * Construct a new menu
       *
       * @param name  Name of the menu
       * @param title Title of the menu
       * @param owner Owner of the menu
       * @throws SMSException If there is already a menu at this location
       */
      public SMSMenu(String name, String title, Plugin owner) {
          this.name = name;
          this.uses = new SMSRemainingUses(this);
          this.attributes = new AttributeCollection(this);
          registerAttributes();
          setAttribute(OWNER, owner == null ? ScrollingMenuSign.CONSOLE_OWNER : "[" + owner.getName() + "]");
          ownerId = ScrollingMenuSign.CONSOLE_UUID;
          setAttribute(TITLE, title);
          autosave = true;
      }
  
      /**
       * Construct a new menu from a frozen configuration object.
       *
       * @param node A ConfigurationSection containing the menu's properties
       * @throws SMSException If there is already a menu at this location
       */
      @SuppressWarnings("unchecked")
      public SMSMenu(ConfigurationSection node) {
          SMSPersistence.mustHaveField(node, "name");
          SMSPersistence.mustHaveField(node, "title");
          SMSPersistence.mustHaveField(node, "owner");
  
          inThaw = true;
  
          this.name = node.getString("name");
          this.uses = new SMSRemainingUses(this, node.getConfigurationSection("usesRemaining"));
          this.attributes = new AttributeCollection(this);
          registerAttributes();
          String id = node.getString("owner_id");
          if (id != null && !id.isEmpty()) {
              this.ownerId = UUID.fromString(id);
          } else {
              this.ownerId = ScrollingMenuSign.CONSOLE_UUID;
          }
  
          // migration of group -> owner_group access in 2.4.0
          if (!node.contains("group") && node.contains(ACCESS) && node.getString(ACCESS).equals("GROUP")) {
              LogUtils.info("menu " + name + ": migrate GROUP -> OWNER_GROUP access");
              node.set("access", "OWNER_GROUP");
          }
  
          for (String k : node.getKeys(false)) {
              if (!node.isConfigurationSection(k) && attributes.hasAttribute(k)) {
                  setAttribute(k, node.getString(k));
              }
          }
  
          List<Map<String, Object>> items = (List<Map<String, Object>>) node.getList("items");
          if (items != null) {
              for (Map<String, Object> item : items) {
                  MemoryConfiguration itemNode = new MemoryConfiguration();
                  // need to expand here because the item may contain a usesRemaining object - item could contain a nested map
                  SMSPersistence.expandMapIntoConfig(itemNode, item);
                  SMSMenuItem menuItem = new SMSMenuItem(this, itemNode);
                  SMSMenuItem actual = menuItem.uniqueItem();
                  if (!actual.getLabel().equals(menuItem.getLabel()))
                      LogUtils.warning("Menu '" + getName() + "': duplicate item '" + menuItem.getLabelStripped() + "' renamed to '" + actual.getLabelStripped() + "'");
                  addItem(actual);
              }
          }
  
          inThaw = false;
          autosave = true;
      }
  
      public void setAttribute(String k, String val) {
          SMSValidate.isTrue(attributes.contains(k), "No such view attribute: " + k);
          attributes.set(k, val);
      }
  
      private void registerAttributes() {
          attributes.registerAttribute(AUTOSORT, false, "Always keep the menu sorted?");
          attributes.registerAttribute(DEFAULT_CMD, "", "Default command to run if item has no command");
          attributes.registerAttribute(OWNER, "", "Player who owns this menu");
          attributes.registerAttribute(GROUP, "", "Permission group for this menu");
          attributes.registerAttribute(TITLE, "", "The menu's displayed title");
          attributes.registerAttribute(ACCESS, SMSAccessRights.ANY, "Who may use this menu");
          attributes.registerAttribute(REPORT_USES, true, "Tell the player when remaining uses have changed?");
      }
  
      public Map<String, Object> freeze() {
          HashMap<String, Object> map = new HashMap<String, Object>();
  
          List<Map<String, Object>> l = new ArrayList<Map<String, Object>>();
          for (SMSMenuItem item : items) {
              l.add(item.freeze());
          }
          for (String key : attributes.listAttributeKeys(false)) {
              if (key.equals(TITLE)) {
                  map.put(key, SMSUtil.escape(attributes.get(key).toString()));
              } else {
                  map.put(key, attributes.get(key).toString());
              }
          }
          map.put("name", getName());
          map.put("items", l);
          map.put("usesRemaining", uses.freeze());
          map.put("owner_id", getOwnerId() == null ? "" : getOwnerId().toString());
  
          return map;
      }
  
      public AttributeCollection getAttributes() {
          return attributes;
      }
  
      /**
       * Get the menu's unique name
       *
       * @return Name of this menu
       */
      public String getName() {
          return name;
      }
  
      /**
       * Get the menu's title string
       *
       * @return The title string
       */
      public String getTitle() {
          return title;
      }
  
      /**
       * Set the menu's title string
       *
       * @param newTitle The new title string
       */
      public void setTitle(String newTitle) {
          attributes.set(TITLE, newTitle);
      }
  
      /**
       * Get the menu's owner string
       *
       * @return Name of the menu's owner
       */
      public String getOwner() {
          return attributes.get(OWNER).toString();
      }
  
      /**
       * Set the menu's owner string.
       *
       * @param owner Name of the menu's owner
       */
      public void setOwner(String owner) {
          attributes.set(OWNER, owner);
      }
  
      /**
       * Get the menu's permission group.
       *
       * @return the group
       */
      public String getGroup() {
          return attributes.get(GROUP).toString();
      }
  
      /**
       * Set the menu's permission group.
       *
       * @param group Name of the menu's group
       */
      public void setGroup(String group) {
          attributes.set(GROUP, group);
      }
  
      public UUID getOwnerId() {
          return ownerId;
      }
  
      public void setOwnerId(UUID ownerId) {
          this.ownerId = ownerId;
      }
  
      /**
       * Get the menu's autosave status - will menus be automatically saved to disk when modified?
       *
       * @return true or false
       */
      public boolean isAutosave() {
          return autosave;
      }
  
      /**
       * Set the menu's autosave status - will menus be automatically saved to disk when modified?
       *
       * @param autosave true or false
       * @return the previous autosave status - true or false
       */
      public boolean setAutosave(boolean autosave) {
          boolean prevAutosave = this.autosave;
          this.autosave = autosave;
          if (autosave) {
              autosave();
          }
          return prevAutosave;
      }
  
      /**
       * Get the menu's autosort status - will menu items be automatically sorted when added?
       *
       * @return true or false
       */
      public boolean isAutosort() {
          return (Boolean) attributes.get(AUTOSORT);
      }
  
      /**
       * Set the menu's autosort status - will menu items be automatically sorted when added?
       *
       * @param autosort true or false
       */
      public void setAutosort(boolean autosort) {
          setAttribute(AUTOSORT, Boolean.toString(autosort));
      }
  
      /**
       * Get the menu's default command.  This command will be used if the menu item
       * being executed has a missing command.
       *
       * @return The default command string
       */
      public String getDefaultCommand() {
          return attributes.get(DEFAULT_CMD).toString();
      }
  
      /**
       * Set the menu's default command.  This command will be used if the menu item
       * being executed has a missing command.
       *
       * @param defaultCommand the default command to set
       */
      public void setDefaultCommand(String defaultCommand) {
          setAttribute(DEFAULT_CMD, defaultCommand);
      }
  
      /**
       * Get a list of all the items in the menu
       *
       * @return A list of the items
       */
      public List<SMSMenuItem> getItems() {
          return items;
      }
  
      /**
       * Get the number of items in the menu
       *
       * @return The number of items
       */
      public int getItemCount() {
          return items.size();
      }
  
      /**
       * Get the item at the given numeric index
       *
       * @param index 1-based numeric index
       * @return The menu item at that index or null if out of range and mustExist is false
       * @throws SMSException if the index is out of range and mustExist is true
       */
      public SMSMenuItem getItemAt(int index, boolean mustExist) {
          if (index < 1 || index > items.size()) {
              if (mustExist) {
                  throw new SMSException("Index " + index + " out of range.");
              } else {
                  return null;
              }
          } else {
              return items.get(index - 1);
          }
      }
  
      /**
       * Get the item at the given numeric index.
       *
       * @param index 1-based numeric index
       * @return the menu item at that index or null if out of range
       */
      public SMSMenuItem getItemAt(int index) {
          return getItemAt(index, false);
      }
  
      /**
       * Get the menu item matching the given label.
       *
       * @param wanted the label to match (case-insensitive)
       * @return the menu item with that label, or null if no matching item
       */
      public SMSMenuItem getItem(String wanted) {
          return getItem(wanted, false);
      }
  
      /**
       * Get the menu item matching the given label
       *
       * @param wanted    The label to match (case-insensitive)
       * @param mustExist If true and the label is not in the menu, throw an exception
       * @return The menu item with that label, or null if no matching item and mustExist is false
       * @throws SMSException if no matching item and mustExist is true
       */
      public SMSMenuItem getItem(String wanted, boolean mustExist) {
          if (items.size() != itemMap.size())
              rebuildItemMap();    // workaround for Heroes 1.4.8 which calls menu.getItems().clear
  
          Integer idx = itemMap.get(ChatColor.stripColor(wanted.replace(FAKE_SPACE, " ")));
          if (idx == null) {
              if (mustExist) {
                  throw new SMSException("No such item '" + wanted + "' in menu " + getName());
              } else {
                  return null;
              }
          }
          return getItemAt(idx);
      }
  
      /**
       * Get the index of the item matching the given label
       *
       * @param wanted The label to match (case-insensitive)
       * @return 1-based item index, or -1 if no matching item
       */
      public int indexOfItem(String wanted) {
          if (items.size() != itemMap.size())
              rebuildItemMap();    // workaround for Heroes 1.4.8 which calls menu.getItems().clear
  
          int index = -1;
          try {
              index = Integer.parseInt(wanted);
          } catch (NumberFormatException e) {
              String l = ChatColor.stripColor(wanted.replace(FAKE_SPACE, " "));
              if (itemMap.containsKey(l))
                  index = itemMap.get(l);
          }
          return index;
      }
  
      /**
       * Append a new item to the menu
       *
       * @param label   Label of the item to add
       * @param command Command to be run when the item is selected
       * @param message Feedback text to be shown when the item is selected
       * @deprecated use {@link #addItem(SMSMenuItem)}
       */
      @Deprecated
      public void addItem(String label, String command, String message) {
          addItem(new SMSMenuItem(this, label, command, message));
      }
  
      /**
       * Append a new item to the menu
       *
       * @param item The item to be added
       */
      public void addItem(SMSMenuItem item) {
          insertItem(items.size() + 1, item);
      }
  
      /**
       * Insert new item in the menu, at the given position.
       *
       * @param pos     the position to insert at
       * @param label   label of the new item
       * @param command command to be run
       * @param message feedback message text
       * @deprecated use {@link #insertItem(int, SMSMenuItem)}
       */
      @Deprecated
      public void insertItem(int pos, String label, String command, String message) {
          insertItem(pos, new SMSMenuItem(this, label, command, message));
      }
  
      /**
       * Insert a new item in the menu, at the given position.
       *
       * @param item The item to insert
       * @param pos  The position to insert (1-based index)
       */
      public void insertItem(int pos, SMSMenuItem item) {
  