/*
* � Copyright IBM Corp. 2013, 2015
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.ibm.domino.commons.model;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import lotus.domino.ACL;
import lotus.domino.ACLEntry;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.Name;
import lotus.domino.NotesException;
import lotus.domino.Session;
import com.ibm.commons.util.StringUtil;
import com.ibm.domino.commons.util.BackendUtil;
/**
* PIM delegate provider.
*
* <p>WARNING: You should never construct an instance of this directly.
* Get an instance of IDelegateProvider from ProviderFactory.
*/
public class DelegateProvider implements IDelegateProvider {
private static final String OWNER_ITEM = "Owner"; // $NON-NLS-1$
private static final String READ_CALENDAR_ITEM = "ReadCalendar"; // $NON-NLS-1$
private static final String WRITE_CALENDAR_ITEM = "WriteCalendar"; // $NON-NLS-1$
private static final String READ_MAIL_ITEM = "ReadMail"; // $NON-NLS-1$
private static final String WRITE_MAIL_ITEM = "WriteMail"; // $NON-NLS-1$
private static final String EDIT_MAIL_ITEM = "EditMail"; // $NON-NLS-1$
private static final String DELETE_MAIL_ITEM = "DeleteMail"; // $NON-NLS-1$
// NOTE: Do not change the order of the items in this array. The getDelegates
// method depends on this order.
private static final String s_items[] = {READ_CALENDAR_ITEM, WRITE_CALENDAR_ITEM, READ_MAIL_ITEM,
WRITE_MAIL_ITEM, EDIT_MAIL_ITEM, DELETE_MAIL_ITEM};
/* (non-Javadoc)
* @see com.ibm.domino.commons.model.IDelegateProvider#get(lotus.domino.Database, java.lang.String)
*/
public Delegate get(Database database, String name) throws ModelException {
Delegate delegate = null;
try {
List<Delegate> list = getDelegates(database);
Iterator<Delegate> iterator = list.iterator();
while ( iterator.hasNext() ) {
Delegate thisDelegate = iterator.next();
if ( thisDelegate.getName().equalsIgnoreCase(name) ) {
delegate = thisDelegate;
break;
}
}
}
catch (NotesException e) {
throw new ModelException("Error getting delegate access", e); // $NLX-DelegateProvider.Errorgettingdelegateaccess-1$
}
return delegate;
}
/* (non-Javadoc)
* @see com.ibm.domino.commons.model.IDelegateProvider#set(lotus.domino.Database, com.ibm.domino.commons.model.Delegate)
*/
public void set(Database database, Delegate delegate) throws ModelException {
Document profile = null;
try {
validateDelegateAccess(delegate.getAccess());
if ( StringUtil.isEmpty(delegate.getName()) ) {
throw new ModelException("A delegate must have a name.", ModelException.ERR_INVALID_INPUT); // $NLX-DelegateProvider.Adelegatemusthaveaname-1$
}
// Update the delegate
profile = profileGet(database);
setImpl(database, delegate, profile);
// Update the profile
if ( delegate.getType() != Delegate.Type.DEFAULT ) {
try {
profileRemoveDelegate(profile, delegate.getName());
profileAddDelegate(profile, delegate);
profile.save();
}
catch (NotesException e) {
// Ignore errors. As long as the ACL is updated, we shouldn't lose sleep
// about the calendar profile.
}
}
}
catch (NotesException e) {
throw new ModelException("Error creating delegate", e); // $NLX-DelegateProvider.Errorcreatingdelegate-1$
}
finally {
BackendUtil.safeRecycle(profile);
}
}
/**
* Does most of the work of updating a delegate.
*
* <p>This version edits the ACL directly. A subclass may use adminp
* to update the delegate.
*
* @param database
* @param delegate
* @throws ModelException
* @throws NotesException
*/
protected void setImpl(Database database, Delegate delegate, Document profile) throws ModelException, NotesException {
ACL acl = null;
try {
if ( !hasManagerAccess(database) ) {
throw new ModelException("Manager access is required to modify a delegate.", ModelException.ERR_NOT_ALLOWED); // $NLX-DelegateProvider.Manageraccessisrequiredtomodifyad-1$
}
acl = database.getACL();
// Find the matching delegate
boolean updated = false;
ACLEntry entry = acl.getFirstEntry();
while ( entry != null ) {
Name no = entry.getNameObject();
if ( delegate.getName().equalsIgnoreCase(no.getAbbreviated()) ) {
// Clear the current access
entry.setLevel(ACL.LEVEL_NOACCESS);
entry.setPublicReader(false);
entry.setPublicWriter(false);
// Apply the new access
aclEntryFromDelegate(delegate, entry);
acl.save();
updated = true;
break;
}
entry = acl.getNextEntry();
}
if ( !updated ) {
throw new ModelException("Delegate not found", ModelException.ERR_NOT_FOUND); // $NLX-DelegateProvider.Delegatenotfound-1$
}
}
finally {
BackendUtil.safeRecycle(acl);
}
}
/* (non-Javadoc)
* @see com.ibm.domino.commons.model.IDelegateProvider#add(lotus.domino.Database, com.ibm.domino.commons.model.Delegate)
*/
public void add(Database database, Delegate delegate) throws ModelException {
Document profile = null;
try {
validateDelegateAccess(delegate.getAccess());
if ( StringUtil.isEmpty(delegate.getName()) ) {
throw new ModelException("A delegate must have a name.", ModelException.ERR_INVALID_INPUT); // $NLX-DelegateProvider.Adelegatemusthaveaname.1-1$
}
// Add the delegate
profile = profileGet(database);
addImpl(database, delegate, profile);
// Update the profile
try {
profileAddDelegate(profile, delegate);
profile.save();
}
catch (NotesException e) {
// Ignore errors. As long as the ACL is updated, we shouldn't lose sleep
// about the calendar profile.
}
}
catch (NotesException e) {
throw new ModelException("Error creating delegate", e); // $NLX-DelegateProvider.Errorcreatingdelegate.1-1$
}
finally {
BackendUtil.safeRecycle(profile);
}
}
/**
* Does most of the work of creating a delegate.
*
* <p>This version edits the ACL directly. A subclass may use adminp
* to createe the delegate.
*
* @param database
* @param delegate
* @throws ModelException
* @throws NotesException
*/
protected void addImpl(Database database, Delegate delegate, Document profile) throws ModelException, NotesException {
ACL acl = null;
try {
if ( !hasManagerAccess(database) ) {
throw new ModelException("Manager access is required to add a delegate.", ModelException.ERR_NOT_ALLOWED); // $NLX-DelegateProvider.Manageraccessisrequiredtoaddadele-1$
}
acl = database.getACL();
String delegateName = delegate.getName();
// Check for conflicts
ACLEntry entry = acl.getFirstEntry();
while ( entry != null ) {
Name no = entry.getNameObject();
if ( delegateName.equalsIgnoreCase(no.getAbbreviated()) ) {
throw new ModelException("A delegate of that name already exists", ModelException.ERR_CONFLICT); // $NLX-DelegateProvider.Adelegateofthatnamealreadyexists-1$
}
entry = acl.getNextEntry();
}
// Create the new entry
entry = acl.createACLEntry(delegateName, ACL.LEVEL_NOACCESS);
aclEntryFromDelegate(delegate, entry);
acl.save();
}
finally {
BackendUtil.safeRecycle(acl);
}
}
/* (non-Javadoc)
* @see com.ibm.domino.commons.model.IDelegateProvider#delete(lotus.domino.Database, java.lang.String)
*/
public void delete(Database database, String name) throws ModelException {
Document profile = null;
if ( Delegate.DEFAULT_NAME.equalsIgnoreCase(name) ) {
throw new ModelException("Cannot remove the default delegate", ModelException.ERR_NOT_ALLOWED); // $NLX-DelegateProvider.Cannotremovethedefaultdelegate-1$
}
try {
// Remove the delegate
profile = profileGet(database);
deleteImpl(database, name, profile);
// Update the profile
try {
profileRemoveDelegate(profile, name);
profile.save();
}
catch (NotesException e) {
// Ignore errors. As long as the ACL is updated, we shouldn't lose sleep
// about the calendar profile.
}
}
catch (NotesException e) {
throw new ModelException("Error deleting delegate", e); // $NLX-DelegateProvider.Errordeletingdelegate-1$
}
finally {
BackendUtil.safeRecycle(profile);
}
}
/**
* Does most of the work of deleting a delegate.
*
* <p>This version edits the ACL directly. A subclass may use adminp
* to delete the delegate.
*
* @param database
* @param name The abbreviated name of the delegate to delete.
* @param owner The canonical name of the mail file owner.
* @throws ModelException
* @throws NotesException
*/
protected void deleteImpl(Database database, String name, Document profile) throws ModelException, NotesException {
ACL acl = null;
try {
if ( !hasManagerAccess(database) ) {
throw new ModelException("Manager access is required to remove a delegate.", ModelException.ERR_NOT_ALLOWED); // $NLX-DelegateProvider.Manageraccessisrequiredtoremovead-1$
}
// Get the ACL
acl = database.getACL();
boolean deleted = false;
ACLEntry entry = acl.getFirstEntry();
// Get the owner of the mailfile
String owner = profile.getItemValueString(OWNER_ITEM);
// Look at each ACL entry
while ( entry != null ) {
Name no = entry.getNameObject();
// If it's a match, delete it
if ( name.equalsIgnoreCase(no.getAbbreviated()) ) {
// But you can't remove the owner's access
if ( owner != null && owner.equalsIgnoreCase(entry.getName()) ) {
throw new ModelException("Cannot remove the owner's access", ModelException.ERR_NOT_ALLOWED); // $NLX-DelegateProvider.Cannotremovetheownersaccess-1$
}
// It's gone
acl.removeACLEntry(name);
acl.save();
deleted = true;
break;
}
entry = acl.getNextEntry();
}
if ( !deleted ) {
throw new ModelException("Delegate not found", ModelException.ERR_NOT_FOUND); // $NLX-DelegateProvider.Delegatenotfound.1-1$
}
}
finally {
BackendUtil.safeRecycle(acl);
}
}
/* (non-Javadoc)
* @see com.ibm.domino.commons.model.IDelegateProvider#getNames(lotus.domino.Database)
*/
public List<Delegate> getList(Database database) throws ModelException {
List<Delegate> list = null;
try {
list = getDelegates(database);
}
catch (NotesException e) {
throw new ModelException("Error getting delegate list", e); // $NLX-DelegateProvider.Errorgettingdelegatelist-1$
}
return list;
}
/* (non-Javadoc)
* @see com.ibm.domino.commons.model.IDelegateProvider#getEffectiveAccess(lotus.domino.Database)
*/
public DelegateAccess getEffectiveAccess(Database database) throws ModelException {
DelegateAccess access = null;
try {
Session session = database.getParent();
String user = session.getEffectiveUserName();
if ( StringUtil.isEmpty(user) ) {
throw new ModelException("Error getting effective user name"); // $NLX-DelegateProvider.Errorgettingeffectiveusername-1$
}
int level = database.queryAccess(user);
int privileges = database.queryAccessPrivileges(user);
DelegateAccess.What what = DelegateAccess.What.NOTHING;
boolean read = false;
boolean create = false;
boolean delete = false;
boolean edit = false;
if ( level < ACL.LEVEL_READER ) {
if ( (privileges & Database.DBACL_READ_PUBLIC_DOCS) != 0 ) {
what = DelegateAccess.What.CALENDAR;
read = true;
if ( (privileges & Database.DBACL_WRITE_PUBLIC_DOCS) != 0 ) {
create = true;
}
}
}
else {
what = DelegateAccess.What.MAIL;
read = true;
if ( level == ACL.LEVEL_AUTHOR ) {
create = true;
}
else if ( level >= ACL.LEVEL_EDITOR ) {
create = true;
edit = true;
}
if ( (privileges & Database.DBACL_DELETE_DOCS) != 0 ) {
delete = true;
}
}
access = new DelegateAccess(what, read, create, delete, edit);
}
catch (NotesException e) {
throw new ModelException("Error getting effective access", e); // $NLX-DelegateProvider.Errorgettingeffectiveaccess-1$
}
return access;
}
/**
* Reads the list of delegates from the ACL.
*
* <p>We don't use this code anymore, but it's kept here for
* sentimental reasons.
*
* @param database
* @return
* @throws NotesException
*/
private List<Delegate> getDelegatesFromAcl(Database database) throws NotesException {
List<Delegate> delegates = new ArrayList<Delegate>();
ACL acl = null;
Document profile = null;
try {
// Get the owner of the mailfile
profile = profileGet(database);
String owner = profile.getItemValueString(OWNER_ITEM);
// Get the ACL
acl = database.getACL();
ACLEntry entry = acl.getFirstEntry();
// Convert each ACL entry to a delegate
while ( entry != null ) {
Delegate delegate = null;
// Convert entry to delegate, unless this is the owner of the mail file
if ( owner == null || !owner.equalsIgnoreCase(entry.getName()) ) {
delegate = getDelegateFromAclEntry(entry);
}
// Add the delegate to the list
if ( delegate != null ) {
delegates.add(delegate);
}
entry = acl.getNextEntry();
}
}
finally {
BackendUtil.safeRecycle(acl);
BackendUtil.safeRecycle(profile);
}
return delegates;
}
private List<Delegate> getDelegates(Database database) throws NotesException {
List<Delegate> delegates = new ArrayList<Delegate>();
Document profile = null;
try {
// Get the calendar profile
profile = profileGet(database);
String owner = profile.getItemValueString(OWNER_ITEM);
// One time init of variables
Session session = database.getParent();
Map<String, Delegate> map = new HashMap<String, Delegate>();
Vector deleteMailValues = null;
// Walk the list of items BACKWARDS (highest access level to lowest)
for ( int i = s_items.length - 1; i > -1; i--) {
String item = s_items[i];
Vector values = profile.getItemValue(item);
if ( DELETE_MAIL_ITEM.equals(item) ) {
deleteMailValues = values;
continue;
}
if ( values != null ) {
// Do for each delegate name
for ( int j = 0; j < values.size(); j++ ) {
String canonicalName = (String)values.get(j);
// Ignore the owner of the mail file
if ( canonicalName.equalsIgnoreCase(owner) ) {
continue;
}
// Is this delegate already accounted for?
if ( map.get(canonicalName) != null ) {
// Yes. Skip it.
continue;
}
// Calculate the access level
Name name = session.createName(canonicalName);
DelegateAccess access = null;
if ( EDIT_MAIL_ITEM.equals(item) ) {
boolean delete = inVector(canonicalName, deleteMailValues);
access = new DelegateAccess(DelegateAccess.What.MAIL, true, true, delete, true);
}
else if ( WRITE_MAIL_ITEM.equals(item) ) {
boolean delete = inVector(canonicalName, deleteMailValues);
access = new DelegateAccess(DelegateAccess.What.MAIL, true, true, delete, false);
}
else if ( READ_MAIL_ITEM.equals(item) ) {
access = new DelegateAccess(DelegateAccess.What.MAIL, true, false, false, false);
}
else if ( WRITE_CALENDAR_ITEM.equals(item) ) {
access = new DelegateAccess(DelegateAccess.What.CALENDAR, true, true, true, true);
}
else if ( READ_CALENDAR_ITEM.equals(item) ) {
access = new DelegateAccess(DelegateAccess.What.CALENDAR, true, false, false, false);
}
// Calculate the delegate type
Delegate.Type type = Delegate.Type.GROUP;
if ( canonicalName.contains("/") ) {
type = Delegate.Type.PERSON;
}
// Add the new delegate
Delegate delegate = new Delegate(name.getAbbreviated(), type, access);
delegates.add(delegate);
map.put(canonicalName, delegate);
}
}
}
}
finally {
BackendUtil.safeRecycle(profile);
}
return delegates;
}
private boolean inVector(String canonicalName, Vector values) {
boolean in = false;
for ( int i = 0; i < values.size(); i++ ) {
String thisName = (String)values.get(i);
if ( thisName.equalsIgnoreCase(canonicalName) ) {
in = true;
break;
}
}
return in;
}
/**
* Translates an ACLEntry to a Delegate.
*
* @param entry
* @return The delegate or null if this entry doesn't map to a real delegate.
* @throws NotesException
*/
private Delegate getDelegateFromAclEntry(ACLEntry entry) throws NotesException {
Delegate delegate = null;
do {
String name = entry.getName();
int userType = entry.getUserType();
Delegate.Type dt = null;
if ( Delegate.DEFAULT_NAME.equals(name) ) {
dt = Delegate.Type.DEFAULT;
}
else {
if ( userType == ACLEntry.TYPE_PERSON ) {
dt = Delegate.Type.PERSON;
}
else if ( userType == ACLEntry.TYPE_PERSON_GROUP || userType == ACLEntry.TYPE_MIXED_GROUP ) {
dt = Delegate.Type.GROUP;
}
else if ( userType == ACLEntry.TYPE_UNSPECIFIED ) {
dt = Delegate.Type.UNSPECIFIED;
}
else {
break;
}
}
DelegateAccess.What what = DelegateAccess.What.NOTHING;
boolean read = false;
boolean create = false;
boolean delete = false;
boolean edit = false;
int level = entry.getLevel();
if ( level == ACL.LEVEL_NOACCESS || level == ACL.LEVEL_DEPOSITOR ) {
if ( entry.isPublicReader() ) {
what = DelegateAccess.What.CALENDAR;
read = true;
if ( entry.isPublicWriter() ) {
create = true;
delete = true;
edit = true;
}
}
}
else {
// Entity has at least reader access
what = DelegateAccess.What.MAIL;
read = true;
if ( level > ACL.LEVEL_READER ) {
create = true;
}
if ( level > ACL.LEVEL_AUTHOR ) {
edit = true;
}
if ( entry.isCanDeleteDocuments() ) {
delete = true;
}
}
if ( what == DelegateAccess.What.NOTHING && dt != Delegate.Type.DEFAULT ) {
// Ignore an entry with no access, unless it's the default entry
break;
}
// Create the delgate object
Name no = entry.getNameObject();
DelegateAccess access = new DelegateAccess(what, read, create, delete, edit);
delegate = new Delegate((no == null) ? name : no.getAbbreviated(),
dt, access);
}
while ( false );
return delegate;
}
/**
* Translates a Delegate to an ACLEntry
*
* @param delegate
* @param entry
* @throws ModelException
* @throws NotesException
*/
private void aclEntryFromDelegate(Delegate delegate, ACLEntry entry) throws ModelException, NotesException {
DelegateAccess.What what = delegate.getAccess().getWhat();
if ( what == DelegateAccess.What.NOTHING ) {
if ( delegate.getType() != Delegate.Type.DEFAULT ) {
throw new ModelException("A delegate must have access to something", ModelException.ERR_INVALID_INPUT); // $NLX-DelegateProvider.Adelegatemusthaveaccesstosomethin-1$
}
}
else if ( what == DelegateAccess.What.CALENDAR ) {
entry.setLevel(ACL.LEVEL_NOACCESS);
entry.setPublicReader(true);
DelegateAccess dc = delegate.getAccess();
if ( dc.isCreate() || dc.isEdit() || dc.isDelete() ) {
entry.setPublicWriter(true);
}
}
else {
int level = ACL.LEVEL_READER;
if ( delegate.getAccess().isEdit() ) {
level = ACL.LEVEL_EDITOR;
}
else if ( delegate.getAccess().isCreate() ) {
level = ACL.LEVEL_AUTHOR;
}
entry.setLevel(level);
if ( level == ACL.LEVEL_AUTHOR ) {
// SPR# XYXZ9AVCT7: Mail creator can also write public docs
entry.setPublicWriter(true);
}
entry.setCanDeleteDocuments(delegate.getAccess().isDelete());
}
int userType = ACLEntry.TYPE_UNSPECIFIED;
Delegate.Type type = delegate.getType();
if ( type == Delegate.Type.PERSON ) {
userType = ACLEntry.TYPE_PERSON;
}
else if ( type == Delegate.Type.GROUP ) {
userType = ACLEntry.TYPE_MIXED_GROUP;
}
entry.setUserType(userType);
}
/**
* Does the user have Manager access to the database?
*
* <p>This method doesn't check the ACL's internet access level. That
* must be checked at a level that knows we are executing in the
* HTTP task.
*
* @param database
* @return
* @throws NotesException
*/
private boolean hasManagerAccess(Database database) throws NotesException {
boolean managerAccess = false;
Session session = database.getParent();
String user = session.getEffectiveUserName();
if ( !StringUtil.isEmpty(user) ) {
int level = database.queryAccess(user);
if ( level >= ACL.LEVEL_MANAGER ) {
managerAccess = true;
}
}
return managerAccess;
}
/**
* Gets an up-to-date copy of the profile document.
*
* @param database
* @return
* @throws NotesException
*/
private Document profileGet(Database database) throws NotesException {
// Open the calendar profile. This returns a cached copy (may not be up to date).
Document profile = database.getProfileDocument("CalendarProfile", null); // $NON-NLS-1$
String unid = profile.getUniversalID();
profile.recycle();
// Open the same document by UNID. This ensures we get the latest copy.
profile = database.getDocumentByUNID(unid);
return profile;
}
/**
* Adds a delegate to the calendar profile.
*
* @param profile
* @param delegate
* @throws NotesException
*/
private void profileAddDelegate(Document profile, Delegate delegate) throws NotesException {
Session session = profile.getParentDatabase().getParent();
Name name = session.createName(delegate.getName());
DelegateAccess da = delegate.getAccess();
String appendItems[] = null;
if ( da.getWhat() == DelegateAccess.What.CALENDAR ) {
if ( da.isCreate() || da.isEdit() || da.isDelete() ) {
appendItems = new String[] {READ_CALENDAR_ITEM, WRITE_CALENDAR_ITEM};
}
else {
appendItems = new String[] {READ_CALENDAR_ITEM};
}
}
else if ( da.getWhat() == DelegateAccess.What.MAIL ) {
if ( da.isEdit() ) {
if ( da.isDelete() ) {
appendItems = new String[] {WRITE_CALENDAR_ITEM, EDIT_MAIL_ITEM, DELETE_MAIL_ITEM};
}
else {
appendItems = new String[] {WRITE_CALENDAR_ITEM, EDIT_MAIL_ITEM};
}
}
else if ( da.isCreate() ) {
if ( da.isDelete() ) {
appendItems = new String[] {WRITE_CALENDAR_ITEM, WRITE_MAIL_ITEM, DELETE_MAIL_ITEM};
}
else {
appendItems = new String[] {WRITE_CALENDAR_ITEM, WRITE_MAIL_ITEM};
}
}
else {
appendItems = new String[] {READ_MAIL_ITEM};
}
}
// Do for each delegate access item
if ( appendItems != null ) {
for ( int i = 0; i < appendItems.length; i++ ) {
// Read the item value
Vector values = profile.getItemValue(appendItems[i]);
// Add the name to the vector
values.add(name.getCanonical());
profile.replaceItemValue(appendItems[i], values);
}
}
}
/**
* Removes a delegate from the calendar profile.
*
* @param profile
* @param delegateName
* @throws NotesException
*/
private void profileRemoveDelegate(Document profile, String delegateName) throws NotesException {
Session session = profile.getParentDatabase().getParent();
// Do for each delegate access item
for ( int i = 0; i < s_items.length; i++ ) {
// Read the item value
Vector values = profile.getItemValue(s_items[i]);
// Remove this name from the vector
for ( int j = 0; j < values.size(); j++ ) {
String strName = (String)values.get(j);
Name name = session.createName(strName);
if ( delegateName.equals(name.getAbbreviated())) {
values.remove(j);
profile.replaceItemValue(s_items[i], values);
break;
}
}
}
}
private void validateDelegateAccess(DelegateAccess access) throws ModelException {
// SPR# BBRL9S9AWZ: You have to have access to something.
if ( access.getWhat() == DelegateAccess.What.NOTHING ) {
throw new ModelException("A delegate must have access to either mail or calendar.", ModelException.ERR_INVALID_INPUT); // $NLX-DelegateProvider.Adelegatemusthaveaccesstosomethin.1-1$
}
// SPR# XZHU9AD9FQ: You can't have just delete access.
if ( access.isDelete() && !(access.isRead() || access.isCreate() || access.isEdit()) ) {
throw new ModelException("You must have at least read access to delete documents.", ModelException.ERR_INVALID_INPUT); // $NLX-DelegateProvider.Cannotdeletedocumentswithoutatlea-1$
}
}
}