# ACLWidget.java

    /*
    ** Module   : ACLWidget.java
    ** Abstract : MVP view.
    **
    ** Copyright (c) 2017-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 HC  20170612 Initial version.
    ** 002 HC  20230427 Added support for file system rights.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.admin.client.application.home.acl;

    import com.goldencode.p2j.admin.*;
    import com.goldencode.p2j.admin.client.*;
    import com.goldencode.p2j.admin.client.application.*;
    import com.goldencode.p2j.admin.client.application.home.*;
    import com.goldencode.p2j.admin.client.application.home.acl.model.*;
    import com.goldencode.p2j.admin.client.editors.*;
    import com.goldencode.p2j.admin.client.widget.*;
    import com.goldencode.p2j.admin.client.widget.dialog.*;
    import com.goldencode.p2j.admin.client.widget.dialog.InputDialog.*;
    import com.goldencode.p2j.admin.shared.*;
    import com.goldencode.p2j.security.*;
    import com.google.gwt.event.dom.client.*;
    import com.google.gwt.regexp.shared.*;
    import com.google.gwt.uibinder.client.*;
    import com.google.gwt.user.client.*;
    import com.google.gwt.user.client.ui.*;
    import com.google.gwt.user.client.ui.Widget;
    import com.google.inject.*;
    import com.gwtplatform.mvp.client.proxy.*;
    import com.gwtplatform.mvp.shared.proxy.*;
    import org.gwtbootstrap3.client.ui.Button;
    import org.gwtbootstrap3.client.ui.CheckBox;
    import org.gwtbootstrap3.client.ui.*;
    import org.gwtbootstrap3.client.ui.TextBox;
    import org.gwtbootstrap3.client.ui.form.validator.*;
    import org.gwtbootstrap3.client.ui.gwt.*;
    import org.gwtbootstrap3.client.ui.html.*;
    import org.gwtbootstrap3.extras.select.client.ui.*;

    import java.util.*;
    import java.util.function.*;

    /**
     * ACL view.
     */
    public class ACLWidget
    extends Composite
    {
       /**
        * GWT binder.
        */
       interface Binder
       extends UiBinder<Container, ACLWidget>
       {}

       /** the string representation of shared instance */
       private static final String SHARED_INSTANCE = "*shared*";

       /** widget */
       @UiField
       Button bRefresh;

       /** widget */
       @UiField
       Button bAdd;

       /** widget */
       @UiField
       Button bDelete;

       /** widget */
       @UiField
       Button bClone;

       /** widget */
       @UiField
       Button bMoveUp;

       /** widget */
       @UiField
       Button bMoveDown;

       /** widget */
       @UiField
       Button bFilter;

       /** widget */
       @UiField
       Button bResequence;

       /** acl grid widget */
       @UiField(provided = true)
       DataGrid grid = GridHelper.create();

       /** widget */
       @UiField
       InlineCheckBox cpcEv;

       /** widget */
       @UiField
       SimplePanel modalPanel;

       /** widget */
       @UiField
       SimplePanel rightsModalPanel;

       /** widget */
       @UiField
       Strong aclTitle;

       /** state definition - initial empty */
       private final boolean[] STATE_INITIAL_EMPTY =
       {
          /*       cpbR,              */       true,
          /*       cpbA,              */       true,
          /*       cpbD,              */ false,
          /*       cpbC,              */ false,
          /*       cpbMu,             */ false,
          /*       cpbMd,             */ false,
          /*       cpbF               */       true,
          /*       cpbS               */ false
       };

       /** state definition - single selection */
       private final boolean[] STATE_ACL_SINGLE =
       {
          /*       cpbR,              */       true,
          /*       cpbA,              */       true,
          /*       cpbD,              */       true,
          /*       cpbC,              */       true,
          /*       cpbMu,             */       true,
          /*       cpbMd,             */       true,
          /*       cpbF               */       true,
          /*       cpbS               */       true
       };

       /** state definition - multiple selection */
       private final boolean[] STATE_ACL_MULTI =
       {
          /*       cpbR,              */       true,
          /*       cpbA,              */       true,
          /*       cpbD,              */       true,
          /*       cpbC,              */ false,
          /*       cpbMu,             */ false,
          /*       cpbMd,             */ false,
          /*       cpbF               */       true,
          /*       cpbS               */ false
       };

       /** Access Control filtered by account. */
       private String account;

       /** Access Control filtered by resource type. */
       private String resourceType;

       /** Access Control filtered by resource instance name. */
       private String resourceName;

       /** The last applied filter */
       private String lastFilter;

       /** The last applied filter */
       private RegExp lastFilterRegexp;

       /** All the ACLs, with no menu filters applied. */
       private List<AclRow> allAcls = null;

       /** The ui state machine */
       private ViewStateMachineImpl stateMachine = null;

       /** Input dialog provider */
       private Provider<InputDialog> inputDialogProvider;

       /** Admin service reference */
       private AdminServiceAsync adm;

       /** ACL grid handle */
       private GridHandle<AclRow> gridHandle;

       /** Effective view flag */
       private boolean effectiveView;

       /** The rights being edited */
       private Rights editRights;

       /** Alarm service */
       private Alarm alarm;

       /** Moda dialogs */
       private ModalDialogs dialogs;

       /** Place manager reference */
       private PlaceManager placeManager;

       /**
        * Ctor.
        *
        * @param   binder
        *          GWT binder.
        * @param   adm
        *          Admin service.
        * @param   alarm
        *          Alarm service.
        * @param   dialogs
        *          Modal dialogs.
        * @param   placeManager
        *          Place manage.
        * @param   inputDialogProvider
        *          Input dialog provider.
        */
       @Inject
       ACLWidget(Binder binder,
                 AdminServiceAsync adm,
                 Alarm alarm,
                 ModalDialogs dialogs,
                 PlaceManager placeManager,
                 Provider<InputDialog> inputDialogProvider)
       {
          this.adm = adm;
          this.alarm = alarm;
          this.dialogs = dialogs;
          this.placeManager = placeManager;
          this.inputDialogProvider = inputDialogProvider;
          initWidget(binder.createAndBindUi(this));

          //noinspection NonJREEmulationClassesInClientCode
          gridHandle = GridHelper.initListGrid(
          grid, true,
          new GridHelper.ColumnInfo<AclRow>("Instance", r -> r.getValueAt(0), (r1, r2) ->
          r1.getValueAt(0).compareTo(r2.getValueAt(0)), aclRow -> handleACLCellEdit(aclRow, 0)),
          new GridHelper.ColumnInfo<>("Resource Type", r -> r.getValueAt(1), (r1, r2) ->
          r1.getValueAt(1).compareTo(r2.getValueAt(1))),
          new GridHelper.ColumnInfo<>("ID", r -> r.getValueAt(2), (r1, r2) ->
          r1.getValueAt(2).compareTo(r2.getValueAt(2)), aclRow -> handleACLCellEdit(aclRow, 2)),
          new GridHelper.ColumnInfo<>("Resource Instance Name", r -> r.getValueAt(3), (r1, r2) ->
          r1.getValueAt(3).compareTo(r2.getValueAt(3)), aclRow -> handleACLCellEdit(aclRow, 3)),
          new GridHelper.ColumnInfo<>("Subjects", r -> r.getValueAt(4), (r1, r2) ->
          r1.getValueAt(4).compareTo(r2.getValueAt(4)), aclRow -> handleACLCellEdit(aclRow, 4)),
          new GridHelper.ColumnInfo<>("Rights", r -> r.getValueAt(5), (r1, r2) ->
          r1.getValueAt(5).compareTo(r2.getValueAt(5)), aclRow -> handleACLCellEdit(aclRow, 5))
          );

          gridHandle.getSelectionModel().addSelectionChangeHandler(
          event ->
          {
             handleACLsSelection();
          });

          // init view state machine
          stateMachine = new ViewStateMachineImpl(new HasEnabled[]
          {
             bRefresh, bAdd, bDelete, bClone, bMoveUp, bMoveDown, bFilter, bResequence
          });

          stateMachine.stateMachine(STATE_INITIAL_EMPTY);
       }

       /**
        * Called when the view is navigated to.
        */
       public void onNavigation()
       {
          PlaceRequest place = placeManager.getCurrentPlaceRequest();
          String resourceType = place.getParameter("r", null);
          String account = place.getParameter("a", null);
          String filter = place.getParameter("f", null);
          String name = place.getParameter("n", null);

          setFilter(resourceType, account, filter, name);
       }

       /**
        * Sets ACL filter.
        *
        * @param   resourceType
        *          Resource type.
        * @param   account
        *          Account id.
        * @param   filter
        *          Filter regular expression.
        * @param   name
        *          Resource instance name.
        */
       public void setFilter(String resourceType, String account, String filter, String name)
       {
          this.resourceType = resourceType;
          this.account = account;
          this.resourceName = name;
          this.lastFilter = filter;

          if (lastFilter == null || lastFilter.trim().isEmpty())
          {
             lastFilterRegexp = null;
          }
          else
          {
             lastFilterRegexp = RegExp.compile(lastFilter);
          }

          updateACLTitle();
          refresh();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bRefresh")
       public void bRefreshClick(ClickEvent event)
       {
          refresh();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bAdd")
       public void bAddClick(ClickEvent event)
       {
          handleAddACL();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bDelete")
       public void bDeleteClick(ClickEvent event)
       {
          handleDeleteSelectedACL();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bClone")
       public void bCloneClick(ClickEvent event)
       {
          handleCloneACLItem();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bMoveUp")
       public void bMoveUpClick(ClickEvent event)
       {
          handleMoveACLUp();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bMoveDown")
       public void bMoveDownClick(ClickEvent event)
       {
          handleMoveACLDown();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bFilter")
       public void bFilterClick(ClickEvent event)
       {
          handleFilter();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("bResequence")
       public void bResequenceClick(ClickEvent event)
       {
          handleResequenceACLs();
       }

       /**
        * Click handler.
        *
        * @param   event
        *          Click event.
        */
       @UiHandler("cpcEv")
       public void cpcEvChange(ChangeEvent event)
       {
          refresh();
       }

       /**
        * Refresh the ACL list from the server.
        */
       public void refresh()
       {
          populateACLList(null);
       }

       /**
        * Grid row selection change handler.
        */
       private void handleACLsSelection()
       {
          Set<AclRow> sel = gridHandle.getSelected();
          if (sel.isEmpty())
          {
             // no selection
             setACLsNoSelection();
          }
          else if (sel.size() == 1)
          {
             // single selection
             setACLsSingleSelection();
          }
          else
          {
             // multiple selection
             setACLsMultiSelection();
          }
       }

       /**
        * Updates the ACL title.
        */
       private void updateACLTitle()
       {
          StringBuilder sb = new StringBuilder();
          sb.append("Access Control");

          boolean and = false;
          if (account != null && !account.trim().isEmpty())
          {
             sb.append(" for account <" + account + ">");
             and = true;
          }

          if (resourceType != null && !resourceType.trim().isEmpty())
          {
             sb.append(" for resource <" + resourceType + ">");
             and = true;
          }

          if (resourceName != null && !resourceName.trim().isEmpty())
          {
             sb.append(" for resource instance name <" + resourceName + ">");
             and = true;
          }

          String prefix = and ? " and" : " for";
          sb.append(prefix + " server <" + ClientContext.instance().getServerName() + ">");

          if (lastFilterRegexp != null)
          {
             sb.append(" filtered by <" + lastFilter + ">");
          }

          aclTitle.setText(sb.toString());
       }

       /**
        * Populate the list of ACLs with fresh data from the server.
        *
        * @param    doneHandler
        *           The done handler
        */
       private void populateACLList(Runnable doneHandler)
       {
          gridHandle.clearGrid();

          // populate the ACL table
          loadAclData(list ->
          {
             if (list != null)
             {
                if (!list.isEmpty())
                {
                   gridHandle.addRows(list, lastFilterRegexp);
                }
             }
             else
             {
                // TODO: beep!
                //client.beep("no ACLs");
             }

             if (doneHandler != null)
             {
                doneHandler.run();
             }
          });
       }

       /**
        * Load the ACL items into {@link AclRow} instances and return them. Any
        * menu filtering is applied at this time.
        * <p>
        * If filtering by resource type is in effect, this method takes into
        * account the state of the Effective View check box and the existence of
        * both the shared and private instances. It disables the check box if only
        * one instance is found, but the check box remains selected.
        *
        * @param   resultConsumer
        *          Called with the result, null is passed when an error occurs.
        */
       private void loadAclData(Consumer<List<AclRow>> resultConsumer)
       {
          // load the ACLs from server
          if (resourceType != null)
          {
             String serverName = ClientContext.instance().getServerName();

             // filtering by resource type
             // figure out the effective versus full view
             adm.listAclInstances(resourceType, new AdminCallback<String[]>(adm, alarm)
             {
                @Override
                public void onDone(String[] found)
                {
                   if (found == null || found.length == 0)
                   {
                      // nothing to show for this resource now
                      resultConsumer.accept(null);
                      return;
                   }

                   List<String> foundList = Arrays.asList(found);
                   boolean isShar = foundList.contains("");
                   boolean isPriv = foundList.contains(serverName);

                   if (!isShar && !isPriv)
                   {
                      // nothing to show for the live server
                      resultConsumer.accept(null);
                      return;
                   }

                      if (isShar && isPriv)
                   {
                      // either effective or full view is possible
                      // the views will be different:
                      // Effective View check box remains enabled and has effect
                      cpcEv.setEnabled(true);
                      effectiveView = cpcEv.getValue();

                      if (effectiveView)
                      {
                         // produce the effective view of ACLs
                         adm.getEffectiveAcls(resourceType, new AdminCallback<AclDef>()
                         {
                            @Override
                            public void onDone(AclDef result)
                            {
                               if (result != null)
                               {
                                  AclDef[] acls = new AclDef[]{result};
                                  loadAclDataStep2(acls, resultConsumer);
                               }
                            }
                         });
                      }
                      else
                      {
                         // produce full view of both shared and private instances
                         MultiCallback mc = new MultiCallback();
                         AdminCallback<AclDef> aSharCb = mc.newCallback();
                         AdminCallback<AclDef> aPrivCb = mc.newCallback();

                         mc.onDone(() ->
                         {
                            if (mc.isSuccess())
                            {
                               AclDef aShar = aSharCb.getResult();
                               AclDef aPriv = aPrivCb.getResult();
                               AclDef[] aslcs = new AclDef[]{aShar, aPriv};
                               loadAclDataStep2(aslcs, resultConsumer);
                            }
                            else
                            {
                               resultConsumer.accept(null);
                            }
                         });

                         adm.getAcl(resourceType, "", aSharCb);
                         adm.getAcl(resourceType, serverName, aPrivCb);
                      }
                   }
                   else
                   {
                      // effective or full view makes no difference:
                      // Effective View check box gets disabled
                      cpcEv.setEnabled(false);

                      String instance = isShar ? "" : serverName;

                      adm.getAcl(resourceType, instance, new AdminCallback<AclDef>()
                      {
                         @Override
                         public void onDone(AclDef result)
                         {
                            if (result != null)
                            {
                               loadAclDataStep2(new AclDef[] {result}, resultConsumer);
                            }
                         }
                      });

                      // control the ACL sort order: oid
                      effectiveView = true;
                   }
                }
             });
          }
          else
          {
             // no filtering by resource type; load full ACLs
             cpcEv.setEnabled(false);
             adm.getAllAcls(null, new AdminCallback<AclDef[]>(adm, alarm)
             {
                @Override
                public void onDone(AclDef[] result)
                {
                   if (result != null)
                   {
                      loadAclDataStep2(result, resultConsumer);
                   }
                }
             });

             // control the ACL sort order: instance, resource type, oid
             effectiveView = false;
          }
       }

       /**
        * Load the ACL items into {@link AclRow} instances and return them. Any
        * menu filtering is applied at this time. This is step 2 of the loading process.
        *
        * @param    acls
        *           The ACL items
        * @param    resultConsumer
        *           Called with the result, null is passed when an error occurs.
        */
       private void loadAclDataStep2(AclDef[] acls, Consumer<List<AclRow>> resultConsumer)
       {
          // client-side filtering
          List<AclRow> list = new ArrayList<>();
          allAcls = new ArrayList<>();

          int errs = 0;

          for (int i = 0; i < acls.length; i++)
          {
             errs += acls[i].errors;

             for (int j = 0; j < acls[i].acls.length; j++)
             {
                Acl acl = acls[i].acls[j];

                boolean ok = account == null;

                Set<String> subjectsSet = new TreeSet<String>();

                StringBuilder subjects = new StringBuilder();
                for (int r = 0; r < acl.subjects.length; r++)
                {
                   String acc = acls[i].subjects.get(acl.subjects[r]);

                   if (acl.subjects[r] == AclDef.ALL_OTHERS)
                   {
                      acc = "all_others";
                   }
                   if (acc == null)
                   {
                      acc = "*not found*";
                   }

                   // filter results by account, if needed
                   if (!ok && acc.equals(account))
                   {
                      ok = true;
                   }

                   subjects.append(acc);

                   if (acl.subjects[r] >= 0)
                   {
                      subjectsSet.add(acc);
                   }

                   if (r < acl.subjects.length - 1)
                   {
                      subjects.append(", ");
                   }
                }

                // filter by resource instance name if set
                if (resourceName != null && ok)
                {
                   ok = acl.name == null || acl.name.equals(resourceName);
                }

                AclRow row = new AclRow(acls[i], acl, subjects.toString(), subjectsSet);

                allAcls.add(row);

                // if the specified account is not among the subjects, do not add
                // it to the table.
                if (ok)
                {
                   list.add(row);
                }
             }
          }

          // report ACLs in errors
          if (errs > 0)
          {
             // TODO: beep
             //String msg = String.format("%d loaded ACLs are with errors", errs);
             //client.beep(msg);
             Window.alert(errs + " loaded ACLs are with errors");
          }
          else
          {
             // TODO
             //client.ok();
          }

          // the entire set is sorted by type and then by id
          Collections.sort(allAcls);
          resultConsumer.accept(list);
       }

       /**
        * Set the state of the screen when no row is selected - some buttons
        * are enabled.
        */
       private void setACLsNoSelection()
       {
          // set enabled state of the list accordingly
          stateMachine.stateMachine(STATE_INITIAL_EMPTY);
       }

       /**
        * Set the state of the screen when a single row is selected - all buttons
        * are enabled.
        */
       private void setACLsSingleSelection()
       {
          // set enabled state of the list accordingly
          stateMachine.stateMachine(STATE_ACL_SINGLE);
       }

       /**
        * Set the state of the screen when multiple rows are selected - some
        * buttons are enabled.
        */
       private void setACLsMultiSelection()
       {
          // set enabled state of the list accordingly
          stateMachine.stateMachine(STATE_ACL_MULTI);
       }

       /**
        * Handle row filtering using a generic filter.
        */
       @SuppressWarnings("unchecked")
       private void handleFilter()
       {
          InputDialog dlg = inputDialogProvider.get();

          String txt  = "Define the filtering specification below. Press the\n" +
          "'OK' button without any specification to remove any\n" +
          "previous filter.";

          Field fInput;
          dlg.build("Filter", txt, new Field[]
          {
             fInput = new Field(null, lastFilter == null ? "" : lastFilter, true)
          });

          dlg.show(modalPanel, spec ->
          {
             if (spec == null)
             {
                return;
             }

             String val = (String) spec.get(fInput);
             if (val.equals(lastFilter))
             {
                return;
             }

             if (isPopup())
             {
                setFilter(resourceType, account, val, resourceName);
             }
             else
             {
                navigateHere("f", val);
             }
          });
       }

       /**
        * Navigates to the view to pass new filter parameters in the view in a form of new browser
        * history entry.
        *
        * @param   param
        *          URL query param name.
        * @param   value
        *          URL quert param value.
        */
       private void navigateHere(String param, String value)
       {
          PlaceRequest place = placeManager.getCurrentPlaceRequest();
          PlaceRequest.Builder pb = new PlaceRequest.Builder().nameToken(place.getNameToken());

          boolean paramFound = false;
          for (String currParam : place.getParameterNames())
          {
             //noinspection NonJREEmulationClassesInClientCode
             if (currParam.equalsIgnoreCase(param))
             {
                paramFound = true;

                if (value == null)
                {
                   continue;
                }

                pb.with(currParam, value);
             }
             else
             {
                pb.with(currParam, place.getParameter(currParam, null));
             }
          }

          if (!paramFound) {
             pb.with(param, value);
          }

          placeManager.revealPlace(pb.build(), true);
       }

       /**
        * Handles edit request of the provided ACL record and the grid column.
        *
        * @param   aclItem
        *          ACL row.
        * @param   cell
        *          Cell index.
        */
       private void handleACLCellEdit(AclRow aclItem, int cell)
       {
          if (cell == 0)
          {
             Field fPrivate;
             Field fShared;

             // move ACL to another instance
             InputDialog dlg = inputDialogProvider.get();
             dlg.build("Move ACL to another Instance", "",
             new Field[]
             {
                fPrivate = new Field("Private Instance", aclItem.getInstance()),
                fShared = new Field("Shared Instance", "".equals(aclItem.getInstance()))
             });

             TextBox text = (TextBox) dlg.getWidget(fPrivate);
             InlineCheckBox check = (InlineCheckBox) dlg.getWidget(fShared);
             text.setEnabled(!check.getValue());
             check.addChangeHandler(event -> { text.setEnabled(!check.getValue()); });

             dlg.show(this.modalPanel, false, vals ->
             {
                if (vals != null)
                {
                   String newInstance = (String) vals.get(fPrivate);
                   Boolean sharedCheck = (Boolean) vals.get(fShared);

                   adm.moveAcl(aclItem.getResourceType(),
                               aclItem.getAcl().origin,
                               aclItem.getAcl().oid,
                               !sharedCheck ? newInstance : "",
                               new AdminCallback<Boolean>()
                   {
                      @Override
                      public void onDone(Boolean result)
                      {
                         if (!result)
                         {
                            alarm.ring();
                         }
                         else
                         {
                            dlg.dismiss();
                         }

                         refresh();
                      }
                   });
                }
             });
          }

          if (cell == 2)
          {
             Field fTargetId;

             // edit ID
             InputDialog dlg = inputDialogProvider.get();
             dlg.build("Move ACL around", "",
             new Field[]
             {
                fTargetId = new Field("Target ID", Integer.class, false)
             });

             dlg.show(this.modalPanel, false, vals ->
             {
                if (vals != null)
                {
                   Integer targetId = (Integer) vals.get(fTargetId);

                   adm.moveAcl(aclItem.getResourceType(),
                               aclItem.getAcl().origin,
                               aclItem.getAcl().oid,
                               targetId,
                               new AdminCallback<Boolean>()
                   {
                      @Override
                      public void onDone(Boolean result)
                      {
                         if (!result)
                         {
                            alarm.ring();
                         }
                         else
                         {
                            dlg.dismiss();
                         }

                         refresh();
                      }
                   });
                }
             });
          }

          if (cell == 3)
          {
             String name = aclItem.isErrorName() ? "" : aclItem.getName();

             Field fName, fRegexp;
             InputDialog dlg = inputDialogProvider.get();
             dlg.build("Change Resource Instance Name", "",
             new Field[]
             {
                fName = new Field(null, name, false),
                fRegexp = new Field("Regexp Match (Uncheck for Exact Match)", !aclItem.getExact())
             });

             dlg.show(this.modalPanel, false, vals ->
             {
                if (vals != null)
                {
                   String instanceName = (String) vals.get(fName);
                   Boolean regexpNature = (Boolean) vals.get(fRegexp);

                   // clear selection before the key field (name) changes, otherwise the selection
                   // will stay in the selection model forever
                   gridHandle.clearSelection();

                   Acl acl = aclItem.getAcl();
                   acl.name   = instanceName;
                   acl.exact  = !regexpNature;

                   // ensure instance name is provided
                   if (acl.origin == null)
                   {
                      acl.origin = aclItem.getAclDef().server;
                   }

                   adm.setAcl(aclItem.getResourceType(),
                              aclItem.getAcl(),
                              Collections.emptyMap(),
                              true, true, false, false,
                              new AdminCallback<Boolean>()
                              {
                                 @Override
                                 public void onDone(Boolean result)
                                 {
                                    if (!result)
                                    {
                                       alarm.ring();
                                    }
                                    else
                                    {
                                       dlg.dismiss();
                                    }

                                    refresh();
                                 }
                              });
                }
             });
          }

          if (cell == 4)
          {
             boolean allOthers = aclItem.getAcl().subjects.length == 1 &&
                                 aclItem.getAcl().subjects[0] == AclDef.ALL_OTHERS;

             loadAllSubjects(options ->
             {
                List<String> subjects = new ArrayList<>(aclItem.getSubjectsSet());

                Field fAll, fSubjects;
                InputDialog dlg = inputDialogProvider.get();
                dlg.build("Change Associated Subjects", "",
                new Field[]
                {
                   fAll = new Field("Associate All Accounts", allOthers),
                   fSubjects = new MultiSelectField(null, options, subjects, false, null)
                });

                CheckBox allOthersCb = (CheckBox) dlg.getWidget(fAll);
                SelectBase select = (SelectBase) dlg.getWidget(fSubjects);

                allOthersCb.addChangeHandler(event ->
                {
                   select.setEnabled(!allOthersCb.getValue());
                });

                dlg.show(this.modalPanel, false, vals ->
                {
                   if (vals != null)
                   {
                      Map<Integer, String> subjMap = new HashMap<>();

                      boolean all = (boolean) vals.get(fAll);
                      int[] subjs;

                      if (all)
                      {
                         subjMap.put(0, "all_others");
                         subjs = new int[] {AclDef.ALL_OTHERS};
                      }
                      else
                      {
                         int i = 0;
                         List<String> subjectList = (List<String>) vals.get(fSubjects);
                         subjs = new int[subjectList.size()];
                         for (String subj : subjectList)
                         {
                            subjs[i] = i;
                            subjMap.put(i++, subj);
                         }
                      }

                      Acl acl = aclItem.getAcl();

                      acl.subjects = subjs;

                      // ensure instance name is provided
                      if (acl.origin == null)
                      {
                         acl.origin = aclItem.getAclDef().server;
                      }

                      adm.setAcl(aclItem.getResourceType(),
                                 aclItem.getAcl(),
                                 subjMap,
                                 false, false, true, false,
                                 new AdminCallback<Boolean>(adm, alarm)
                                 {
                                    @Override
                                    public void onDone(Boolean result)
                                    {
                                       dlg.dismiss();
                                       refresh();
                                    }
                                 });
                   }
                });
             });
          }

          if (cell == 5)
          {
             String resourceName = aclItem.getName();
             boolean exact = aclItem.getExact();
             String type = aclItem.getResourceType();
             Rights source = aclItem.getAcl().rights;
             if (aclItem.isErrorRights())
             {
                source = null;
             }

             editRights(resourceName, exact, type, source, rights ->
             {
                if (rights != null)
                {
                   aclItem.getAcl().rights = rights;
                   aclItem.clearErrorRights();

                   // ensure instance name is provided
                   if (aclItem.getAcl().origin == null)
                   {
                      aclItem.getAcl().origin = aclItem.getAclDef().server;
                   }

                   adm.setAcl(
                      type,
                      aclItem.getAcl(),
                      Collections.EMPTY_MAP,
                      false,
                      false,
                      false,
                      true,
                      new AdminCallback<Boolean>(adm, alarm)
                      {
                         @Override
                         public void onDone(Boolean result)
                         {
                            refresh();
                         }
                      });
                }
             });
          }
       }

       /**
        * Loads all subjects.
        *
        * @param   result
        *          The result consumer.
        */
       private void loadAllSubjects(Consumer<List<Option>> result)
       {
          MultiCallback mc = new MultiCallback();
          AdminCallback<TaggedName[]> usersCb = mc.newCallback();
          AdminCallback<TaggedName[]> groupsCb = mc.newCallback();
          AdminCallback<TaggedName[]> procsCb = mc.newCallback();

          mc.onDone(() ->
          {
             if (!mc.isSuccess())
             {
                return;
             }

             List<TaggedName> users = new ArrayList<>(Arrays.asList(usersCb.getResult()));
             List<TaggedName> procs = new ArrayList<>(Arrays.asList(procsCb.getResult()));
             List<TaggedName> groups = new ArrayList<>(Arrays.asList(groupsCb.getResult()));
             List<TaggedName> allSubjects = new ArrayList<>();
             allSubjects.addAll(users);
             allSubjects.addAll(procs);
             allSubjects.addAll(groups);
             Collections.sort(allSubjects);

             List<Option> optsResult = new ArrayList<>();

             for (TaggedName subject : allSubjects)
             {
                String type;

                if (users.contains(subject))
                {
                   type = "user";
                }
                else if (groups.contains(subject))
                {
                   type = "group";
                }
                else if (procs.contains(subject))
                {
                   type = "process";
                }
                else
                {
                   continue;
                }

                Option opt = new Option();
                String name = subject.getName();
                opt.setValue(name);
                opt.setText(name);
                opt.setSubtext("(" + type + ", " + subject.getTag() + ")");
                optsResult.add(opt);
             }

             result.accept(optsResult);
          });

          adm.listUsers(usersCb);
          adm.listGroups(groupsCb);
          adm.listProcesses(procsCb);
       }

       /**
        * Handle Add ACL Item button press. Prepares and displays "Add ACL" dialog.
        */
       private void handleAddACL()
       {
          MultiCallback mc = new MultiCallback();
          AdminCallback<String[]> instances = mc.newCallback();
          AdminCallback<AdminDef> admDef = mc.newCallback();
          AdminCallback<List<Option>> subjects = mc.newCallback();

          mc.onDone(() ->
          {
             AdminDef admDefRes = admDef.getResult();
             String[] instancesRes = instances.getResult();
             List<String> instancesList = new ArrayList<>(instancesRes.length);
             for (int i = 0; i < instancesRes.length; i++)
             {
                String in = instancesRes[i].length() == 0 ? SHARED_INSTANCE : instancesRes[i];
                instancesList.add(in);
             }

             if (admDefRes == null || instancesRes == null)
             {
                return;
             }

             AclRow sel = getFirstSelectedRow();

             String defInstance;
             if (sel != null)
             {
                defInstance = sel.getInstance();
             }
             else
             {
                defInstance = instancesRes.length > 0 ? instancesRes[0] : null;
             }

             String defType = null;
             if (sel != null)
             {
                defType = sel.getResourceType();
             }

             List<String> types = new ArrayList<>(admDefRes.customAclEditors.size());
             for (String type : admDefRes.customAclEditors)
             {
                if (defType == null)
                {
                   defType = type;
                }
                types.add(type);
             }

             int id = nextId(false, true);

             InputGroupButton rightsAddon = new InputGroupButton();
             Button editRightsButton = new Button("Edit");
             rightsAddon.add(editRightsButton);

             Field fServer, fRes, fId, fName, fRegexp, fSubjects, fAll, fRights;
             InputDialog dlg = inputDialogProvider.get();
             dlg.build("Add ACL Item", "",
             new Item[]
             {
                // allow empty server entry
                fServer = new Field("Server this ACL item is for", defInstance, true)
                          .withSuggestions(instancesList),
                fRes = new SelectField("Resource Type", types, defType),
                fId = new Field("Access Control ID", id >= 0 ? id : Integer.class, false,
                                new DecimalMinValidator(0)),
                fRegexp = new Field("Regexp Match (Uncheck for Exact Match)", false),
                fName = new Field("Resource Instance Name", "", false),
                new InputDialog.Span
                (
                   fSubjects = new MultiSelectField("Subjects", subjects.getResult(),
                                                 Collections.emptyList(), false, null),
                   fAll = new Field("Associate All Accounts", true)
                ),
                fRights = new Field("Rights", "", false).withAddon(rightsAddon),
             });

             Select resourceBox = (Select) dlg.getWidget(fRes);
             CheckBox regexpBox = (CheckBox) dlg.getWidget(fRegexp);
             MultipleSelect subjBox = (MultipleSelect) dlg.getWidget(fSubjects);
             CheckBox allOthersCb = (CheckBox) dlg.getWidget(fAll);
             subjBox.setEnabled(false);

             allOthersCb.addChangeHandler(event ->
             {
                subjBox.setEnabled(!allOthersCb.getValue());
             });

             TextBox rightsBox = (TextBox) dlg.getWidget(fRights);
             rightsBox.setReadOnly(true);

             Consumer<String> resetRights = val ->
             {
                RightsEditorContext ctxt = RightsEditorContext.instance();
                ctxt.setInputDialog(inputDialogProvider.get());
                ctxt.setParent(rightsModalPanel);
                RightsEditor ed = ctxt.getEditor(val);
                editRights = ed.createDefaultRights();
                rightsBox.setValue(editRights.toString());
             };

             resetRights.accept(defType);

             Select resTypeBox = (Select) dlg.getWidget(fRes);
             resTypeBox.addValueChangeHandler(val ->
             {
                if (val != null && val.getValue() != null)
                {
                   resetRights.accept(val.getValue());
                }
                else
                {
                   resetRights.accept("");
                }
             });

             resourceBox.addValueChangeHandler(event ->
             {
                resetRights.accept(resTypeBox.getValue());
             });

             regexpBox.addValueChangeHandler(event ->
             {
                resetRights.accept(resTypeBox.getValue());
             });

             editRightsButton.addClickHandler(event ->
             {
                editRights(dlg.getValue(fName, ""),
                           !regexpBox.getValue(),
                           resTypeBox.getValue(),
                           editRights, rights ->
                {
                   if (rights != null)
                   {
                      editRights = rights;
                      rightsBox.setValue(editRights.toString());
                   }
                });
             });

             dlg.show(this.modalPanel, false, vals ->
             {
                if (vals != null)
                {
                   Map<Integer, String> subjMap = new HashMap<>();

                   boolean all = (boolean) vals.get(fAll);
                   int[] subjs;

                   if (all)
                   {
                      subjMap.put(0, "all_others");
                      subjs = new int[] {AclDef.ALL_OTHERS};
                   }
                   else
                   {
                      int i = 0;
                      List<String> subjectList = (List<String>) vals.get(fSubjects);
                      subjs = new int[subjectList.size()];
                      for (String subj : subjectList)
                      {
                         subjs[i] = i;
                         subjMap.put(i++, subj);
                      }
                   }

                   String instance = (String) vals.get(fServer);
                   String resType = (String) vals.get(fRes);
                   int aclId = (int) vals.get(fId);
                   String resName = (String) vals.get(fName);
                   boolean regexpNature = (boolean) vals.get(fRegexp);

                   if (!instancesList.contains(instance))
                   {
                      dialogs.showYesNo(
                         "Warning",
                         "About to create a new ACL instance " + instance + ". Continue?",
                         MessageType.WARNING,
                         r ->
                         {
                            // yes
                            if (r == 0)
                            {
                               addACLStep2(instance,
                                           resType,
                                           aclId,
                                           resName,
                                           regexpNature,
                                           subjs,
                                           subjMap,
                                           editRights,
                                           (res) ->
                                           {
                                              if (res)
                                              {
                                                 dlg.dismiss();
                                              }
                                           });
                            }
                         });
                   }
                   else
                   {
                      addACLStep2(instance,
                                  resType,
                                  aclId,
                                  resName,
                                  regexpNature,
                                  subjs,
                                  subjMap,
                                  editRights,
                                  (res) ->
                                  {
                                     if (res)
                                     {
                                        dlg.dismiss();
                                     }
                                  });
                   }
                }
             });
          });

          adm.listAclInstances(instances);
          adm.getAdminDef(admDef);
          loadAllSubjects(l -> subjects.onSuccess(l));
       }

       /**
        * 2nd phase of new ACL dialog.
        *
        * @param   instance
        *          Instance name.
        * @param   resType
        *          Resource type.
        * @param   aclId
        *          Acl id.
        * @param   resName
        *          Resource name.
        * @param   regexpNature
        *          Regexp nature flag.
        * @param   subjs
        *          Subject ids.
        * @param   subjMap
        *          Subject map.
        * @param   rights
        *          Rights object.
        * @param   doneHandler
        *          Done handler.
        */
       private void addACLStep2(String instance,
                                String resType,
                                int aclId,
                                String resName,
                                boolean regexpNature,
                                int[] subjs,
                                Map<Integer, String> subjMap,
                                Rights rights,
                                Consumer<Boolean> doneHandler)
       {
          //noinspection NonJREEmulationClassesInClientCode
          if (instance.equalsIgnoreCase(SHARED_INSTANCE))
          {
             instance = "";
          }

          Acl acl = new Acl();
          acl.origin   = instance;
          acl.oid      = aclId;
          acl.name     = resName;
          acl.exact    = !regexpNature;
          acl.subjects = subjs;
          acl.rights   = rights;

          adm.addAcl(resType, acl, subjMap, new AdminCallback<Boolean>(adm, alarm)
          {
             @Override
             public void onDone(Boolean result)
             {
                doneHandler.accept(result != null && result);
                refresh();
             }
          });
       }

       /**
        * Compute another ID based on the ID of the selected item.
        *
        * @param    up
        *           <code>true</code> if the new ID must be lower than the item's
        *           ID. <code>false</code> if the ID must be greater than the
        *           item's ID.
        * @param    current
        *           <code>true</code> if the selected item is considered the
        *           origin. <code>false</code> if the prior/next item is used,
        *           depending on the direction (up/down).
        *
        * @return   the new ID
        */
       private int nextId(boolean up, boolean current)
       {
          // find out the first selected acl row based on the defaultly sorted list
          AclRow selRow = getFirstSelectedRow();
          if (selRow == null)
          {
             return -1;
          }

          // check to see whether the selected row is the boundary
          int dir = (up ? -1 : 1);

          int selIdx = allAcls.indexOf(selRow);
          int sid = selRow.getId();

          boolean boundary = false;
          AclRow immedNext = null;
          int immedIdx = -1;
          int iid = -1;

          if (selIdx == 0 && up)
          {
             boundary = true;
          }
          else if (selIdx == allAcls.size() - 1 && !up)
          {
             boundary = true;
          }
          else
          {
             immedIdx = selIdx + dir;
             immedNext = allAcls.get(immedIdx);
             iid = immedNext.getId();

             if (!immedNext.getInstance().equals(selRow.getInstance()) ||
             !immedNext.getResourceType().equals(selRow.getResourceType()))
             {
                boundary = true;
             }
          }

          // handle the boundary case
          if (boundary)
          {
             if (up)
             {
                if (sid == 1)
                {
                   // no room to move to
                   return -1;
                }

                return sid / 2;
             }

             return sid + 100;
          }

          // when moving, the next item is used (current flag is false)
          // when adding/cloning, the current item must be used

          if (current)
          {
             //  place the new ID between the initial selection and immediate next
             int distance = Math.abs(iid - sid);
             if (distance == 1)
             {
                // no room to move to
                return -1;
             }

             return sid + dir * distance / 2;
          }

          // check to see whether the immediate next row is the boundary
          int nextIdx = -1;
          AclRow nextRow = null;
          int nid = -1;

          if (immedIdx == 0 && up)
          {
             boundary = true;
          }
          else if (immedIdx == allAcls.size() - 1 && !up)
          {
             boundary = true;
          }
          else
          {
             nextIdx = immedIdx + dir;
             nextRow = allAcls.get(nextIdx);
             nid = nextRow.getId();

             if (!nextRow.getInstance().equals(selRow.getInstance()) ||
             !nextRow.getResourceType().equals(selRow.getResourceType()))
             {
                boundary = true;
             }
          }

          // handle the immediate next boundary case
          if (boundary)
          {
             if (up)
             {
                if (iid == 1)
                {
                   // no room to move to
                   return -1;
                }

                return iid / 2;
             }

             return iid + 100;
          }

          // found two siblings, place the new ID between them
          int distance = Math.abs(nid - iid);
          if (distance == 1)
          {
             // no room to move to
             return -1;
          }

          return iid + dir * distance / 2;
       }

       /**
        * Returns the first selected row.
        *
        * @return  the first selected row
        */
       private AclRow getFirstSelectedRow()
       {
          Set<AclRow> selSet = gridHandle.getSelected();
          if (selSet.isEmpty())
          {
             return null;
          }

          AclRow selRow = null;
          for (AclRow r : allAcls)
          {
             if (selSet.contains(r))
             {
                selRow = r;
                break;
             }
          }

          return selRow;
       }

       /**
        * Handle Delete Selected ACL button press.
        */
       private void handleDeleteSelectedACL()
       {
          Set<AclRow> selected = gridHandle.getSelected();

          // ask confirmation
          String quest = null;
          String title = null;

          if (selected.size() == 1)
          {
             AclRow row = selected.iterator().next();
             StringBuilder sb = new StringBuilder();
             sb.append("Deleting ACL ").append(row.getId()).append(" in <")
               .append(row.getValueAt(0)).append("><").append(row.getValueAt(1))
               .append(">. Continue?");
             quest = sb.toString();
             title = "Deleting item";
          }
          else
          {
             quest = "Deleting " + selected.size() + " items. Continue?";
             title = "Deleting items";
          }

          dialogs.showYesNo(title, quest, MessageType.QUESTION, r ->
          {
             // yes
             if (r == 0)
             {
                deletedACL(selected);
             }
          });
       }

       /**
        * Deletes the supplied set of acls.
        *
        * @param   rows
        *          The acls to delete.
        */
       private void deletedACL(Set<AclRow> rows)
       {
          MultiCallback mc = new MultiCallback(adm, alarm);
          mc.onDone(() -> refresh());

          List<AdminCallback> cbs = new ArrayList<>(rows.size());

          for (int i = 0; i < rows.size(); i++)
          {
             cbs.add(mc.newCallback());
          }

          int i = 0;
          for (AclRow row : rows)
          {
             // attempt deletion
             adm.deleteAcl(row.getResourceType(),
                           row.getInstance(),
                           row.getId(),
                           cbs.get(i++));
          }
       }

       /**
        * Clone the selected ACL item.
        */
       private void handleCloneACLItem()
       {
          int id = nextId(false, true);
          AclRow row = getFirstSelectedRow();

          Field fId;
          InputDialog dlg = inputDialogProvider.get();
          dlg.build("Clone ACL Item", "",
          new Field[]
          {
             fId = new Field("Clone ID", id >= 0 ? id : Integer.class, false, new DecimalMinValidator(0)),
          });

          dlg.show(this.modalPanel, false, vals ->
          {
             if (vals != null)
             {
                int newId = (int) vals.get(fId);
                adm.cloneAcl(
                   row.getResourceType(),
                   row.getInstance(),
                   row.getId(),
                   newId,
                   new AdminCallback<Boolean>(adm, alarm)
                   {
                     @Override
                     public void onDone(Boolean result)
                     {
                        dlg.dismiss();
                        populateACLList(() ->
                         {
                            AclRow nRow = locateAcl(row.getResourceType(), row.getInstance(), newId);
                            if (nRow != null)
                            {
                               gridHandle.select(nRow);
                            }
                         });
                     }
                   });
             }
          });
       }

       /**
        * Move the ACL item up or down.
        *
        * @param    up
        *           <code>true</code> if the item needs to be moved up.
        */
       private void moveACLItem(boolean up)
       {
          AclRow row = getFirstSelectedRow();

          // safety check
          if (row == null)
          {
             return;
          }

          // compute a new ID candidate
          int newId = nextId(up, false);
          if (newId == -1)
          {
             alarm.ring("No room to move to");
             return;
          }

          // move without confirmation
          moveACL(row, newId);
       }

       /**
        * Handle Move ACL Up button press - if not first, it will move the ACL
        * one position up.
        */
       private void handleMoveACLUp()
       {
          moveACLItem(true);
       }

       /**
        * Handle Move ACL Down button press - if not last, it will move the ACL
        * one position down.
        */
       private void handleMoveACLDown()
       {
          moveACLItem(false);
       }

       /**
        * Move an ACL to a directly specified position.
        *
        * @param    row
        *           the ACL to move
        * @param    newId
        *           new position for the ACL
        */
       private void moveACL(AclRow row, int newId)
       {
          // attempt movement
          adm.moveAcl(row.getResourceType(),
                      row.getInstance(),
                      row.getId(),
                      newId,
                      new AdminCallback<Boolean>(adm, alarm)
                      {
                         @Override
                         public void onDone(Boolean result)
                         {
                            populateACLList(() ->
                            {
                               AclRow nRow = locateAcl(row.getResourceType(), row.getInstance(), newId);
                               if (nRow != null)
                               {
                                  gridHandle.select(nRow);
                               }
                            });
                         }
                      });
       }

       /**
        * Looks up the list of all ACLs and locates the one with given resource
        * type, instance and ID.
        *
        * @param    resourceType
        *           resource type to search within
        * @param    instanceName
        *           ACL instance name to search within
        * @param    id
        *           ACL id of interest
        *
        * @return   the requested ACL or <code>null</code>
        */
       private AclRow locateAcl(String resourceType, String instanceName, int id)
       {
          for (int i = 0; i < allAcls.size(); i++)
          {
             AclRow row = allAcls.get(i);
             if (!instanceName.equals(row.getInstance()))
             {
                continue;
             }
             if (!resourceType.equals(row.getResourceType()))
             {
                continue;
             }
             if (id == row.getId())
             {
                return row;
             }
          }

          return null;
       }

       /**
        * Clone the selected ACL item.
        */
       private void handleResequenceACLs()
       {
          int id = nextId(false, true);
          AclRow row = getFirstSelectedRow();

          String title = "Resequence " + row.getResourceType() + " ACLs in " +
                         row.getValueAt(0) + " instance";

          Field fStart, fInc;

          InputDialog dlg = inputDialogProvider.get();
          dlg.build(title, "",
          new Field[]
          {
             fStart = new Field("Starting Number", Integer.class, false, new DecimalMinValidator(0)),
             fInc = new Field("Increment By", Integer.class, false, new DecimalMinValidator(1))
          });

          dlg.show(this.modalPanel, false, vals ->
          {
             if (vals != null)
             {
                int startId = (int) vals.get(fStart);
                int step = (int) vals.get(fInc);

                adm.resequenceAcls(row.getResourceType(), row.getInstance(), startId, step,
                new AdminCallback<Boolean>(adm, alarm)
                {
                   @Override
                   public void onDone(Boolean result)
                   {
                      dlg.dismiss();
                      refresh();
                   }
                });
             }
          });
       }

       /**
        * Shows right editor.
        *
        * @param   resourceName
        *          Resource name.
        * @param   exact
        *          Exact match flag.
        * @param   type
        *          Resource type.
        * @param   rights
        *          The rights object.
        * @param   doneHandler
        *          Done handler.
        */
       private void editRights(String resourceName,
                               boolean exact,
                               String type,
                               Rights rights,
                               Consumer<Rights> doneHandler)
       {
          adm.getAdminDef(new AdminCallback<AdminDef>(adm, alarm)
          {
             @Override
             public void onDone(AdminDef result)
             {
                if (result == null)
                {
                   doneHandler.accept(null);
                   return;
                }

                RightsEditorContext ctxt = RightsEditorContext.instance();
                ctxt.setInputDialog(inputDialogProvider.get());
                ctxt.setParent(rightsModalPanel);
                ctxt.setModalDialogs(dialogs);
                RightsEditor ed = ctxt.getEditor(type);
                if (ed == null)
                {
                   alarm.ring("Unknown editor of type " + type);
                   doneHandler.accept(null);
                   return;
                }

                Rights source = rights;
                if (source == null)
                {
                   source = ed.createDefaultRights();
                }

                ed.initialize(result.resourceDescription.get(type));

                ed.edit(resourceName, exact, source,
                rights ->
                {
                  doneHandler.accept(rights);
                },
                () -> doneHandler.accept(null));
             }
          });
       }

       /**
        * Checks whether the widget is displayed in a popup.
        *
        * @return  <code>true</code> if in popup.
        */
       private boolean isPopup()
       {
          Widget parent = getParent();
          while(parent != null)
          {
             if (parent instanceof PopupPanel)
             {
                return true;
             }

             parent = parent.getParent();
          }

          return false;
       }
    }