org.springframework.security.acls.jdbc.BasicLookupStrategy Maven / Gradle / Ivy
/*
* Copyright 2004, 2005, 2006, 2017 Acegi Technology Pty Limited
*
* 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
*
* https://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 org.springframework.security.acls.jdbc;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.sql.DataSource;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.security.acls.domain.AccessControlEntryImpl;
import org.springframework.security.acls.domain.AclAuthorizationStrategy;
import org.springframework.security.acls.domain.AclImpl;
import org.springframework.security.acls.domain.AuditLogger;
import org.springframework.security.acls.domain.DefaultPermissionFactory;
import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PermissionFactory;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.AccessControlEntry;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.AclCache;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.Permission;
import org.springframework.security.acls.model.PermissionGrantingStrategy;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.acls.model.UnloadedSidException;
import org.springframework.security.util.FieldUtils;
import org.springframework.util.Assert;
/**
* Performs lookups in a manner that is compatible with ANSI SQL.
*
* NB: This implementation does attempt to provide reasonably optimised lookups - within
* the constraints of a normalised database and standard ANSI SQL features. If you are
* willing to sacrifice either of these constraints (e.g. use a particular database
* feature such as hierarchical queries or materalized views, or reduce normalisation) you
* are likely to achieve better performance. In such situations you will need to provide
* your own custom LookupStrategy
. This class does not support subclassing,
* as it is likely to change in future releases and therefore subclassing is unsupported.
*
* There are two SQL queries executed, one in the lookupPrimaryKeys method and
* one in lookupObjectIdentities. These are built from the same select and "order
* by" clause, using a different where clause in each case. In order to use custom schema
* or column names, each of these SQL clauses can be customized, but they must be
* consistent with each other and with the expected result set generated by the the
* default values.
*
* @author Ben Alex
*/
public class BasicLookupStrategy implements LookupStrategy {
private static final String DEFAULT_SELECT_CLAUSE_COLUMNS = "select acl_object_identity.object_id_identity, "
+ "acl_entry.ace_order, " + "acl_object_identity.id as acl_id, " + "acl_object_identity.parent_object, "
+ "acl_object_identity.entries_inheriting, " + "acl_entry.id as ace_id, " + "acl_entry.mask, "
+ "acl_entry.granting, " + "acl_entry.audit_success, " + "acl_entry.audit_failure, "
+ "acl_sid.principal as ace_principal, " + "acl_sid.sid as ace_sid, "
+ "acli_sid.principal as acl_principal, " + "acli_sid.sid as acl_sid, " + "acl_class.class ";
private static final String DEFAULT_SELECT_CLAUSE_ACL_CLASS_ID_TYPE_COLUMN = ", acl_class.class_id_type ";
private static final String DEFAULT_SELECT_CLAUSE_FROM = "from acl_object_identity "
+ "left join acl_sid acli_sid on acli_sid.id = acl_object_identity.owner_sid "
+ "left join acl_class on acl_class.id = acl_object_identity.object_id_class "
+ "left join acl_entry on acl_object_identity.id = acl_entry.acl_object_identity "
+ "left join acl_sid on acl_entry.sid = acl_sid.id " + "where ( ";
public static final String DEFAULT_SELECT_CLAUSE = DEFAULT_SELECT_CLAUSE_COLUMNS + DEFAULT_SELECT_CLAUSE_FROM;
public static final String DEFAULT_ACL_CLASS_ID_SELECT_CLAUSE = DEFAULT_SELECT_CLAUSE_COLUMNS
+ DEFAULT_SELECT_CLAUSE_ACL_CLASS_ID_TYPE_COLUMN + DEFAULT_SELECT_CLAUSE_FROM;
private static final String DEFAULT_LOOKUP_KEYS_WHERE_CLAUSE = "(acl_object_identity.id = ?)";
private static final String DEFAULT_LOOKUP_IDENTITIES_WHERE_CLAUSE = "(acl_object_identity.object_id_identity = ? and acl_class.class = ?)";
public static final String DEFAULT_ORDER_BY_CLAUSE = ") order by acl_object_identity.object_id_identity"
+ " asc, acl_entry.ace_order asc";
private final AclAuthorizationStrategy aclAuthorizationStrategy;
private PermissionFactory permissionFactory = new DefaultPermissionFactory();
private final AclCache aclCache;
private final PermissionGrantingStrategy grantingStrategy;
private final JdbcTemplate jdbcTemplate;
private int batchSize = 50;
private final Field fieldAces = FieldUtils.getField(AclImpl.class, "aces");
private final Field fieldAcl = FieldUtils.getField(AccessControlEntryImpl.class, "acl");
// SQL Customization fields
private String selectClause = DEFAULT_SELECT_CLAUSE;
private String lookupPrimaryKeysWhereClause = DEFAULT_LOOKUP_KEYS_WHERE_CLAUSE;
private String lookupObjectIdentitiesWhereClause = DEFAULT_LOOKUP_IDENTITIES_WHERE_CLAUSE;
private String orderByClause = DEFAULT_ORDER_BY_CLAUSE;
private AclClassIdUtils aclClassIdUtils;
/**
* Constructor accepting mandatory arguments
* @param dataSource to access the database
* @param aclCache the cache where fully-loaded elements can be stored
* @param aclAuthorizationStrategy authorization strategy (required)
*/
public BasicLookupStrategy(DataSource dataSource, AclCache aclCache,
AclAuthorizationStrategy aclAuthorizationStrategy, AuditLogger auditLogger) {
this(dataSource, aclCache, aclAuthorizationStrategy, new DefaultPermissionGrantingStrategy(auditLogger));
}
/**
* Creates a new instance
* @param dataSource to access the database
* @param aclCache the cache where fully-loaded elements can be stored
* @param aclAuthorizationStrategy authorization strategy (required)
* @param grantingStrategy the PermissionGrantingStrategy
*/
public BasicLookupStrategy(DataSource dataSource, AclCache aclCache,
AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy grantingStrategy) {
Assert.notNull(dataSource, "DataSource required");
Assert.notNull(aclCache, "AclCache required");
Assert.notNull(aclAuthorizationStrategy, "AclAuthorizationStrategy required");
Assert.notNull(grantingStrategy, "grantingStrategy required");
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.aclCache = aclCache;
this.aclAuthorizationStrategy = aclAuthorizationStrategy;
this.grantingStrategy = grantingStrategy;
this.aclClassIdUtils = new AclClassIdUtils();
this.fieldAces.setAccessible(true);
this.fieldAcl.setAccessible(true);
}
private String computeRepeatingSql(String repeatingSql, int requiredRepetitions) {
Assert.isTrue(requiredRepetitions > 0, "requiredRepetitions must be > 0");
String startSql = this.selectClause;
String endSql = this.orderByClause;
StringBuilder sqlStringBldr = new StringBuilder(
startSql.length() + endSql.length() + requiredRepetitions * (repeatingSql.length() + 4));
sqlStringBldr.append(startSql);
for (int i = 1; i <= requiredRepetitions; i++) {
sqlStringBldr.append(repeatingSql);
if (i != requiredRepetitions) {
sqlStringBldr.append(" or ");
}
}
sqlStringBldr.append(endSql);
return sqlStringBldr.toString();
}
@SuppressWarnings("unchecked")
private List readAces(AclImpl acl) {
try {
return (List) this.fieldAces.get(acl);
}
catch (IllegalAccessException ex) {
throw new IllegalStateException("Could not obtain AclImpl.aces field", ex);
}
}
private void setAclOnAce(AccessControlEntryImpl ace, AclImpl acl) {
try {
this.fieldAcl.set(ace, acl);
}
catch (IllegalAccessException ex) {
throw new IllegalStateException("Could not or set AclImpl on AccessControlEntryImpl fields", ex);
}
}
private void setAces(AclImpl acl, List aces) {
try {
this.fieldAces.set(acl, aces);
}
catch (IllegalAccessException ex) {
throw new IllegalStateException("Could not set AclImpl entries", ex);
}
}
/**
* Locates the primary key IDs specified in "findNow", adding AclImpl instances with
* StubAclParents to the "acls" Map.
* @param acls the AclImpls (with StubAclParents)
* @param findNow Long-based primary keys to retrieve
* @param sids
*/
private void lookupPrimaryKeys(final Map acls, final Set findNow, final List sids) {
Assert.notNull(acls, "ACLs are required");
Assert.notEmpty(findNow, "Items to find now required");
String sql = computeRepeatingSql(this.lookupPrimaryKeysWhereClause, findNow.size());
Set parentsToLookup = this.jdbcTemplate.query(sql, (ps) -> setKeys(ps, findNow),
new ProcessResultSet(acls, sids));
// Lookup the parents, now that our JdbcTemplate has released the database
// connection (SEC-547)
if (parentsToLookup.size() > 0) {
lookupPrimaryKeys(acls, parentsToLookup, sids);
}
}
private void setKeys(PreparedStatement ps, Set findNow) throws SQLException {
int i = 0;
for (Long toFind : findNow) {
i++;
ps.setLong(i, toFind);
}
}
/**
* The main method.
*
* WARNING: This implementation completely disregards the "sids" argument! Every item
* in the cache is expected to contain all SIDs. If you have serious performance needs
* (e.g. a very large number of SIDs per object identity), you'll probably want to
* develop a custom {@link LookupStrategy} implementation instead.
*
* The implementation works in batch sizes specified by {@link #batchSize}.
* @param objects the identities to lookup (required)
* @param sids the SIDs for which identities are required (ignored by this
* implementation)
* @return a Map where keys represent the {@link ObjectIdentity} of the
* located {@link Acl} and values are the located {@link Acl} (never null
* although some entries may be missing; this method should not throw
* {@link NotFoundException}, as a chain of {@link LookupStrategy}s may be used to
* automatically create entries if required)
*/
@Override
public final Map readAclsById(List objects, List sids) {
Assert.isTrue(this.batchSize >= 1, "BatchSize must be >= 1");
Assert.notEmpty(objects, "Objects to lookup required");
// Map
// contains FULLY loaded Acl objects
Map result = new HashMap<>();
Set currentBatchToLoad = new HashSet<>();
for (int i = 0; i < objects.size(); i++) {
final ObjectIdentity oid = objects.get(i);
boolean aclFound = false;
// Check we don't already have this ACL in the results
if (result.containsKey(oid)) {
aclFound = true;
}
// Check cache for the present ACL entry
if (!aclFound) {
Acl acl = this.aclCache.getFromCache(oid);
// Ensure any cached element supports all the requested SIDs
// (they should always, as our base impl doesn't filter on SID)
if (acl != null) {
Assert.state(acl.isSidLoaded(sids),
"Error: SID-filtered element detected when implementation does not perform SID filtering "
+ "- have you added something to the cache manually?");
result.put(acl.getObjectIdentity(), acl);
aclFound = true;
}
}
// Load the ACL from the database
if (!aclFound) {
currentBatchToLoad.add(oid);
}
// Is it time to load from JDBC the currentBatchToLoad?
if ((currentBatchToLoad.size() == this.batchSize) || ((i + 1) == objects.size())) {
if (currentBatchToLoad.size() > 0) {
Map loadedBatch = lookupObjectIdentities(currentBatchToLoad, sids);
// Add loaded batch (all elements 100% initialized) to results
result.putAll(loadedBatch);
// Add the loaded batch to the cache
for (Acl loadedAcl : loadedBatch.values()) {
this.aclCache.putInCache((AclImpl) loadedAcl);
}
currentBatchToLoad.clear();
}
}
}
return result;
}
/**
* Looks up a batch of ObjectIdentity
s directly from the database.
*
* The caller is responsible for optimization issues, such as selecting the identities
* to lookup, ensuring the cache doesn't contain them already, and adding the returned
* elements to the cache etc.
*
* This subclass is required to return fully valid Acl
s, including
* properly-configured parent ACLs.
*/
private Map lookupObjectIdentities(final Collection objectIdentities,
List sids) {
Assert.notEmpty(objectIdentities, "Must provide identities to lookup");
// contains Acls with StubAclParents
Map acls = new HashMap<>();
// Make the "acls" map contain all requested objectIdentities
// (including markers to each parent in the hierarchy)
String sql = computeRepeatingSql(this.lookupObjectIdentitiesWhereClause, objectIdentities.size());
Set parentsToLookup = this.jdbcTemplate.query(sql,
(ps) -> setupLookupObjectIdentitiesStatement(ps, objectIdentities), new ProcessResultSet(acls, sids));
// Lookup the parents, now that our JdbcTemplate has released the database
// connection (SEC-547)
if (parentsToLookup.size() > 0) {
lookupPrimaryKeys(acls, parentsToLookup, sids);
}
// Finally, convert our "acls" containing StubAclParents into true Acls
Map resultMap = new HashMap<>();
for (Acl inputAcl : acls.values()) {
Assert.isInstanceOf(AclImpl.class, inputAcl, "Map should have contained an AclImpl");
Assert.isInstanceOf(Long.class, ((AclImpl) inputAcl).getId(), "Acl.getId() must be Long");
Acl result = convert(acls, (Long) ((AclImpl) inputAcl).getId());
resultMap.put(result.getObjectIdentity(), result);
}
return resultMap;
}
private void setupLookupObjectIdentitiesStatement(PreparedStatement ps, Collection objectIdentities)
throws SQLException {
int i = 0;
for (ObjectIdentity oid : objectIdentities) {
// Determine prepared statement values for this iteration
String type = oid.getType();
// No need to check for nulls, as guaranteed non-null by
// ObjectIdentity.getIdentifier() interface contract
String identifier = oid.getIdentifier().toString();
// Inject values
ps.setString((2 * i) + 1, identifier);
ps.setString((2 * i) + 2, type);
i++;
}
}
/**
* The final phase of converting the Map
of AclImpl
* instances which contain StubAclParent
s into proper, valid
* AclImpl
s with correct ACL parents.
* @param inputMap the unconverted AclImpl
s
* @param currentIdentity the currentAcl
that we wish to convert (this
* may be
*/
private AclImpl convert(Map inputMap, Long currentIdentity) {
Assert.notEmpty(inputMap, "InputMap required");
Assert.notNull(currentIdentity, "CurrentIdentity required");
// Retrieve this Acl from the InputMap
Acl uncastAcl = inputMap.get(currentIdentity);
Assert.isInstanceOf(AclImpl.class, uncastAcl, "The inputMap contained a non-AclImpl");
AclImpl inputAcl = (AclImpl) uncastAcl;
Acl parent = inputAcl.getParentAcl();
if ((parent != null) && parent instanceof StubAclParent) {
// Lookup the parent
StubAclParent stubAclParent = (StubAclParent) parent;
parent = convert(inputMap, stubAclParent.getId());
}
// Now we have the parent (if there is one), create the true AclImpl
AclImpl result = new AclImpl(inputAcl.getObjectIdentity(), inputAcl.getId(), this.aclAuthorizationStrategy,
this.grantingStrategy, parent, null, inputAcl.isEntriesInheriting(), inputAcl.getOwner());
// Copy the "aces" from the input to the destination
// Obtain the "aces" from the input ACL
List aces = readAces(inputAcl);
// Create a list in which to store the "aces" for the "result" AclImpl instance
List acesNew = new ArrayList<>();
// Iterate over the "aces" input and replace each nested
// AccessControlEntryImpl.getAcl() with the new "result" AclImpl instance
// This ensures StubAclParent instances are removed, as per SEC-951
for (AccessControlEntryImpl ace : aces) {
setAclOnAce(ace, result);
acesNew.add(ace);
}
// Finally, now that the "aces" have been converted to have the "result" AclImpl
// instance, modify the "result" AclImpl instance
setAces(result, acesNew);
return result;
}
/**
* Creates a particular implementation of {@link Sid} depending on the arguments.
* @param sid the name of the sid representing its unique identifier. In typical ACL
* database schema it's located in table {@code acl_sid} table, {@code sid} column.
* @param isPrincipal whether it's a user or granted authority like role
* @return the instance of Sid with the {@code sidName} as an identifier
*/
protected Sid createSid(boolean isPrincipal, String sid) {
if (isPrincipal) {
return new PrincipalSid(sid);
}
return new GrantedAuthoritySid(sid);
}
/**
* Sets the {@code PermissionFactory} instance which will be used to convert loaded
* permission data values to {@code Permission}s. A {@code DefaultPermissionFactory}
* will be used by default.
* @param permissionFactory
*/
public final void setPermissionFactory(PermissionFactory permissionFactory) {
this.permissionFactory = permissionFactory;
}
public final void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
/**
* The SQL for the select clause. If customizing in order to modify column names,
* schema etc, the other SQL customization fields must also be set to match.
* @param selectClause the select clause, which defaults to
* {@link #DEFAULT_SELECT_CLAUSE}.
*/
public final void setSelectClause(String selectClause) {
this.selectClause = selectClause;
}
/**
* The SQL for the where clause used in the lookupPrimaryKey method.
*/
public final void setLookupPrimaryKeysWhereClause(String lookupPrimaryKeysWhereClause) {
this.lookupPrimaryKeysWhereClause = lookupPrimaryKeysWhereClause;
}
/**
* The SQL for the where clause used in the lookupObjectIdentities method.
*/
public final void setLookupObjectIdentitiesWhereClause(String lookupObjectIdentitiesWhereClause) {
this.lookupObjectIdentitiesWhereClause = lookupObjectIdentitiesWhereClause;
}
/**
* The SQL for the "order by" clause used in both queries.
*/
public final void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public final void setAclClassIdSupported(boolean aclClassIdSupported) {
if (aclClassIdSupported) {
Assert.isTrue(this.selectClause.equals(DEFAULT_SELECT_CLAUSE),
"Cannot set aclClassIdSupported and override the select clause; "
+ "just override the select clause");
this.selectClause = DEFAULT_ACL_CLASS_ID_SELECT_CLAUSE;
}
}
public final void setConversionService(ConversionService conversionService) {
this.aclClassIdUtils = new AclClassIdUtils(conversionService);
}
private class ProcessResultSet implements ResultSetExtractor> {
private final Map acls;
private final List sids;
ProcessResultSet(Map acls, List sids) {
Assert.notNull(acls, "ACLs cannot be null");
this.acls = acls;
this.sids = sids; // can be null
}
/**
* Implementation of {@link ResultSetExtractor#extractData(ResultSet)}. Creates an
* {@link Acl} for each row in the {@link ResultSet} and ensures it is in member
* field acls. Any {@link Acl} with a parent will have the parents id
* returned in a set. The returned set of ids may requires further processing.
* @param rs The {@link ResultSet} to be processed
* @return a list of parent IDs remaining to be looked up (may be empty, but never
* null)
* @throws SQLException
*/
@Override
public Set extractData(ResultSet rs) throws SQLException {
Set parentIdsToLookup = new HashSet<>(); // Set of parent_id Longs
while (rs.next()) {
// Convert current row into an Acl (albeit with a StubAclParent)
convertCurrentResultIntoObject(this.acls, rs);
// Figure out if this row means we need to lookup another parent
long parentId = rs.getLong("parent_object");
if (parentId != 0) {
// See if it's already in the "acls"
if (this.acls.containsKey(parentId)) {
continue; // skip this while iteration
}
// Now try to find it in the cache
MutableAcl cached = BasicLookupStrategy.this.aclCache.getFromCache(parentId);
if ((cached == null) || !cached.isSidLoaded(this.sids)) {
parentIdsToLookup.add(parentId);
}
else {
// Pop into the acls map, so our convert method doesn't
// need to deal with an unsynchronized AclCache
this.acls.put(cached.getId(), cached);
}
}
}
// Return the parents left to lookup to the caller
return parentIdsToLookup;
}
/**
* Accepts the current ResultSet
row, and converts it into an
* AclImpl
that contains a StubAclParent
* @param acls the Map we should add the converted Acl to
* @param rs the ResultSet focused on a current row
* @throws SQLException if something goes wrong converting values
* @throws ConversionException if can't convert to the desired Java type
*/
private void convertCurrentResultIntoObject(Map acls, ResultSet rs) throws SQLException {
Long id = rs.getLong("acl_id");
// If we already have an ACL for this ID, just create the ACE
Acl acl = acls.get(id);
if (acl == null) {
// Make an AclImpl and pop it into the Map
// If the Java type is a String, check to see if we can convert it to the
// target id type, e.g. UUID.
Serializable identifier = (Serializable) rs.getObject("object_id_identity");
identifier = BasicLookupStrategy.this.aclClassIdUtils.identifierFrom(identifier, rs);
ObjectIdentity objectIdentity = new ObjectIdentityImpl(rs.getString("class"), identifier);
Acl parentAcl = null;
long parentAclId = rs.getLong("parent_object");
if (parentAclId != 0) {
parentAcl = new StubAclParent(parentAclId);
}
boolean entriesInheriting = rs.getBoolean("entries_inheriting");
Sid owner = createSid(rs.getBoolean("acl_principal"), rs.getString("acl_sid"));
acl = new AclImpl(objectIdentity, id, BasicLookupStrategy.this.aclAuthorizationStrategy,
BasicLookupStrategy.this.grantingStrategy, parentAcl, null, entriesInheriting, owner);
acls.put(id, acl);
}
// Add an extra ACE to the ACL (ORDER BY maintains the ACE list order)
// It is permissible to have no ACEs in an ACL (which is detected by a null
// ACE_SID)
if (rs.getString("ace_sid") != null) {
Long aceId = rs.getLong("ace_id");
Sid recipient = createSid(rs.getBoolean("ace_principal"), rs.getString("ace_sid"));
int mask = rs.getInt("mask");
Permission permission = BasicLookupStrategy.this.permissionFactory.buildFromMask(mask);
boolean granting = rs.getBoolean("granting");
boolean auditSuccess = rs.getBoolean("audit_success");
boolean auditFailure = rs.getBoolean("audit_failure");
AccessControlEntryImpl ace = new AccessControlEntryImpl(aceId, acl, recipient, permission, granting,
auditSuccess, auditFailure);
// Field acesField = FieldUtils.getField(AclImpl.class, "aces");
List aces = readAces((AclImpl) acl);
// Add the ACE if it doesn't already exist in the ACL.aces field
if (!aces.contains(ace)) {
aces.add(ace);
}
}
}
}
private static class StubAclParent implements Acl {
private final Long id;
StubAclParent(Long id) {
this.id = id;
}
Long getId() {
return this.id;
}
@Override
public List getEntries() {
throw new UnsupportedOperationException("Stub only");
}
@Override
public ObjectIdentity getObjectIdentity() {
throw new UnsupportedOperationException("Stub only");
}
@Override
public Sid getOwner() {
throw new UnsupportedOperationException("Stub only");
}
@Override
public Acl getParentAcl() {
throw new UnsupportedOperationException("Stub only");
}
@Override
public boolean isEntriesInheriting() {
throw new UnsupportedOperationException("Stub only");
}
@Override
public boolean isGranted(List permission, List sids, boolean administrativeMode)
throws NotFoundException, UnloadedSidException {
throw new UnsupportedOperationException("Stub only");
}
@Override
public boolean isSidLoaded(List sids) {
throw new UnsupportedOperationException("Stub only");
}
}
}