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;
   }
}