diff --git a/src/main/java/org/caosdb/server/database/access/AbstractAccess.java b/src/main/java/org/caosdb/server/database/access/AbstractAccess.java index 9d00a876e8db21446b88b6ab00dfeb12ee043c0d..1d30bd3fee7187b9c44378e5b94c1c6d97287f00 100644 --- a/src/main/java/org/caosdb/server/database/access/AbstractAccess.java +++ b/src/main/java/org/caosdb/server/database/access/AbstractAccess.java @@ -77,7 +77,13 @@ public abstract class AbstractAccess<T extends TransactionInterface> implements public void setUseCache(final Boolean useCache) { this.useCache = useCache; } - + /** + * Whether the transaction allows to use the query cache or other caches. This is controlled by + * the "cache" flag. + * + * @see {@link NoCache} + * @return true if caching is encouraged. + */ @Override public boolean useCache() { return this.useCache; diff --git a/src/main/java/org/caosdb/server/query/Backreference.java b/src/main/java/org/caosdb/server/query/Backreference.java index fec64b19b761418a4da148ca4adff9d9145960af..7d551fd45e761a54d2c2508dcbb1b98ffc72891c 100644 --- a/src/main/java/org/caosdb/server/query/Backreference.java +++ b/src/main/java/org/caosdb/server/query/Backreference.java @@ -130,7 +130,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callApplyBackRef.setNull(2, VARCHAR); } if (this.propertiesTable != null) { // propertiesTable - getQuery().filterEntitiesWithoutRetrievePermission(this.propertiesTable); + getQuery().filterIntermediateResult(this.propertiesTable); callApplyBackRef.setString(3, this.propertiesTable); this.statistics.put("propertiesTable", this.propertiesTable); this.statistics.put( @@ -140,7 +140,7 @@ public class Backreference implements EntityFilterInterface, QueryInterface { callApplyBackRef.setNull(3, VARCHAR); } if (this.entitiesTable != null) { // entitiesTable - getQuery().filterEntitiesWithoutRetrievePermission(this.entitiesTable); + getQuery().filterIntermediateResult(this.entitiesTable); callApplyBackRef.setString(4, this.entitiesTable); this.statistics.put("entitiesTable", this.entitiesTable); this.statistics.put( diff --git a/src/main/java/org/caosdb/server/query/Query.java b/src/main/java/org/caosdb/server/query/Query.java index 07a165a352ba9a07a345018e7d287cd961f4a00b..81a08a9dbecbd28195172df55dfdd600ae24e53b 100644 --- a/src/main/java/org/caosdb/server/query/Query.java +++ b/src/main/java/org/caosdb/server/query/Query.java @@ -50,12 +50,12 @@ import org.caosdb.api.entity.v1.MessageCode; import org.caosdb.datetime.UTCDateTime; import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; +import org.caosdb.server.accessControl.Principal; import org.caosdb.server.caching.Cache; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException; import org.caosdb.server.database.backend.implementation.MySQL.MySQLHelper; import org.caosdb.server.database.backend.transaction.RetrieveSparseEntity; -import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.database.misc.DBHelper; import org.caosdb.server.database.misc.TransactionBenchmark; import org.caosdb.server.entity.Entity; @@ -70,6 +70,8 @@ import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.query.CQLParser.CqContext; import org.caosdb.server.query.CQLParsingErrorListener.ParsingError; import org.caosdb.server.transaction.TransactionInterface; +import org.caosdb.server.transaction.WriteTransaction; +import org.caosdb.server.utils.ResultSetIterator; import org.jdom2.Element; import org.slf4j.Logger; @@ -211,14 +213,17 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } } - public static class IdVersionPair { - public IdVersionPair(final Integer id, final String version) { + /** A data class for storing triplets of (Entity ID, version hash, ACL string) */ + public static class IdVersionAclTriplet { + public IdVersionAclTriplet(final Integer id, final String version, final String acl) { this.id = id; this.version = version; + this.acl = acl; } public Integer id; public String version; + public String acl; @Override public String toString() { @@ -230,9 +235,16 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac @Override public boolean equals(final Object obj) { - if (obj instanceof IdVersionPair) { - final IdVersionPair that = (IdVersionPair) obj; - return this.id == that.id && this.version == that.version; + if (obj instanceof IdVersionAclTriplet) { + final IdVersionAclTriplet that = (IdVersionAclTriplet) obj; + // checking ID and version hash should be sufficient + if (this.id == that.id && this.version == that.version) { + if (this.acl != that.acl) { + throw new RuntimeException("Implementation error! ACL should not differ"); + } + return true; + } + ; } return false; } @@ -249,7 +261,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac .equalsIgnoreCase("FALSE"); private final Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); - List<IdVersionPair> resultSet = null; + List<IdVersionAclTriplet> resultSet = null; private final String query; private Pattern entity = null; private Role role = null; @@ -265,6 +277,18 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac private final ArrayList<ToElementable> messages = new ArrayList<>(); private Access access; private boolean versioned = false; + /** + * This key-value cache stores lists of of (id, version hash, acl string) triplets. Those values + * are the result sets of queries. The keys are created such that they are different for different + * for different queries (@see {@link #getCacheKey}). The key includes realm and username of a + * subject, if the query result must not be shared among users. If intermediate permission checks + * are done (e.g. in a subproperty query filter), the query result will be stored using a user + * specific key. The final permission check has not yet been applied to the result set that is + * stored in the cache. This allows (some) cache entries to be shared among users since the final + * check is applied after the retrieval of the result set from the cache (@see {@link + * filterEntitiesWithoutRetrievePermission}) The cache is invalidated whenever there is a write + * operation (@see {@link #clearCache} which is called in the {@link WriteTransaction#commit}). + */ private static ICacheAccess<String, Serializable> cache = Cache.getCache("HIGH_LEVEL_QUERY_CACHE"); /** Cached=true means that the results of this query have actually been pulled from the cache. */ @@ -274,7 +298,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * query evaluation contains complex permission checking which could only be cached on a per-user * basis (maybe in the future). */ - private boolean cachable = true; + private boolean filteredIntermediateResult = false; /** * Tags the query cache and is renewed each time the cache is being cleared, i.e. each time the @@ -367,17 +391,16 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac return this.sourceSet; } catch (final SQLException e) { e.printStackTrace(); - throw new TransactionException(e); + throw new QueryException(e); } } /** - * Finds all QueryTemplates in the resultSet and applies them to the same resultSet. The ids of + * Finds all QueryTemplates in the resultSet and applies them to the same resultSet. The IDs of * the QueryTemplates themselves are then removed from the resultSet. If the current user doesn't * have the RETRIEVE:ENTITY permission for a particular QueryTemplate it will be ignored. * * @param resultSet - * @throws SQLException * @throws QueryException */ public void applyQueryTemplates(final QueryInterface query, final String resultSet) @@ -415,7 +438,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac union(query, resultSet, subResultSet); } } catch (final SQLException e) { - throw new TransactionException(e); + throw new QueryException(e); } } @@ -567,7 +590,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * * <ol> * <li>FIND * -> FIND ENTITY (which basically prevents to copy the complete entity table just to - * read out the ids immediately). + * read out the IDs immediately). * </ol> */ public void optimize() { @@ -593,33 +616,47 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } /** - * Generate a SQL statement which reads out the resulting ids (and version ids if `versioned` is - * true). + * Generate a SQL statement which reads out the resulting IDs and ACL strings (and version IDs if + * `versioned` is true). * - * <p>If the parameter `resultSetTableName` is "entities" actually the entity_version table is - * used to fetch all ids. + * <p>There are four variants: Where the parameter `resultSetTableName` is "entities" and + * otherwise and where `versioned` is true and otherwise. * * @param resultSetTableName name of the table with all the resulting entities * @param versioned whether the query was versioned * @return an SQL statement - * @throws QueryException */ private String generateSelectStatementForResultSet( final String resultSetTableName, final boolean versioned) { + // TODO remove the entities.role part when + // https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/245 is resolved if (resultSetTableName.equals("entities")) { - return "SELECT entity_id AS id" - + (versioned ? ", version AS version" : "") - + " FROM entity_version" - + (versioned ? "" : " WHERE `_iversion` = 1"); + final String baseStatement = + "SELECT entities.id, entity_acl.acl FROM entities INNER JOIN entity_acl ON entity_acl.id=entities.acl WHERE entities.role!='DOMAIN'"; + if (!versioned) { + return baseStatement + ";"; + } + // if versioned, the statement is surrounded with another SELECT and JOIN + return ("SELECT id, acl, version FROM (" + + baseStatement + + ") AS tmp JOIN entity_version ON entity_version.entity_id=tmp.id;"); + } else { + if (!versioned) { + return (" SELECT tmp.id, entity_acl.acl FROM " + + " (SELECT results.id AS id, entities.acl AS acl_id FROM `" + + resultSetTableName + + "` AS results JOIN entities ON results.id=entities.id WHERE entities.role!='DOMAIN') AS tmp" + + " JOIN entity_acl ON entity_acl.id=tmp.acl_id") + + ";"; + } + // if versioned, the statement is surrounded with another SELECT and JOIN + return ("SELECT tmp2.id, acl, version FROM( SELECT tmp.id, entity_acl.acl, tmp._iversion AS _iversion FROM " + + " (SELECT results.id AS id, entities.acl AS acl_id, results._iversion AS _iversion FROM `" + + resultSetTableName + + "` AS results JOIN entities ON results.id=entities.id) AS tmp" + + " JOIN entity_acl ON entity_acl.id=tmp.acl_id) as tmp2 " + + "join entity_version on (entity_version.entity_id=tmp2.id AND tmp2._iversion = entity_version._iversion);"); } - return "SELECT results.id AS id" - + (versioned ? ", ev.version AS version" : "") - + " FROM `" - + resultSetTableName - + "` AS results" - + (versioned - ? " JOIN entity_version AS ev ON (results.id = ev.entity_id AND results._iversion = ev._iversion)" - : ""); } /** @@ -630,17 +667,19 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * @return list of results of this query. * @throws QueryException */ - private List<IdVersionPair> getResultSet(final String resultSetTableName, final boolean versioned) - throws QueryException { + private List<IdVersionAclTriplet> getResultSet( + final String resultSetTableName, final boolean versioned) throws QueryException { ResultSet finishResultSet = null; try { final String sql = generateSelectStatementForResultSet(resultSetTableName, versioned); final PreparedStatement finish = getConnection().prepareStatement(sql); finishResultSet = finish.executeQuery(); - final List<IdVersionPair> rs = new LinkedList<>(); + final List<IdVersionAclTriplet> rs = new LinkedList<>(); while (finishResultSet.next()) { final String version = versioned ? finishResultSet.getString("version") : null; - rs.add(new IdVersionPair(finishResultSet.getInt("id"), version)); + final String acl = finishResultSet.getString("acl"); + + rs.add(new IdVersionAclTriplet(finishResultSet.getInt("id"), version, acl)); } return rs; } catch (final SQLException e) { @@ -657,14 +696,43 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } /** - * Whether the transaction allows this query instance to use the query cache. This is controlled - * by the "cache" flag. - * - * @see {@link NoCache} - * @return true if caching is encouraged. + * Try to set the `resultSet` member variable using the values stored in the high level query + * cache. */ - private boolean useCache() { - return getAccess().useCache(); + private void getResultFromCache() { + // try key with username and realm + // TODO include this again to activate the user-specific caching + // this.resultSet = getCached(getCacheKey(true)); + if (this.resultSet == null) { + // try key without username and realm + this.resultSet = getCached(getCacheKey(false)); + } + } + /** Store the content of `resultSet` member in the high level query cache. */ + private void storeResultInCache() { + // Decide whether user specific cache needs to be used or not + // Currently, this is solely determined via filteredIntermediateResult. + if (this.filteredIntermediateResult) { + // TODO include this again to activate user-specific caching + // cacheItem(getCacheKey(true), this.resultSet); + } else { + cacheItem(getCacheKey(false), this.resultSet); + } + } + /** Fill entities from `resultSet` into `container`. */ + private void fillContainerWithResult() { + if (this.container != null && this.type == Type.FIND) { + for (final IdVersionAclTriplet t : this.resultSet) { + + final Entity e = new RetrieveEntity(t.id, t.version); + + // if query has select-clause: + if (this.selections != null && !this.selections.isEmpty()) { + e.addSelections(this.selections); + } + this.container.add(e); + } + } } /** @@ -674,59 +742,60 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * * @param access * @return - * @throws ParsingException + * @throws ParsingException, QueryException */ - public Query execute(final Access access) throws ParsingException { - parse(); - setAccess(access); - if (useCache()) { - this.resultSet = getCached(getCacheKey()); - } - - if (this.resultSet == null) { - executeNoCache(access); - if (this.cachable) { - setCache(getCacheKey(), this.resultSet); + public Query execute(final Access access) throws ParsingException, QueryException { + try { + parse(); + setAccess(access); + if (access.useCache()) { + getResultFromCache(); } - this.logger.debug("Uncached query {}", this.query); - } else { - this.logger.debug("Using cached result for {}", this.query); - this.cached = true; - } - - this.resultSet = filterEntitiesWithoutRetrievePermission(this.resultSet); - - // Fill resulting entities into container - if (this.container != null && this.type == Type.FIND) { - for (final IdVersionPair p : this.resultSet) { + if (this.resultSet != null) { + this.logger.debug("Using cached result for {}", this.query); + this.cached = true; - if (p.id > 99) { - final Entity e = new RetrieveEntity(p.id, p.version); - - // if query has select-clause: - if (this.selections != null && !this.selections.isEmpty()) { - e.addSelections(this.selections); - } - this.container.add(e); - } + } else { + executeQueryInBackend(access); + storeResultInCache(); + this.logger.debug("Uncached query {}", this.query); } + + this.resultSet = filterEntitiesWithoutRetrievePermission(this.resultSet); + this.resultSet = removeInternalEntitiesFromResultSet(); + fillContainerWithResult(); + } catch (final SQLException e) { + e.printStackTrace(); + throw new QueryException(e); } return this; } - /** Remove all cached queries from the cache. */ public static void clearCache() { cacheETag = UUID.randomUUID().toString(); cache.clear(); } + /** There are internal Entities (with ID<100) that should never be returned. */ + private List<IdVersionAclTriplet> removeInternalEntitiesFromResultSet() { + + final List<IdVersionAclTriplet> filtered = new ArrayList<>(); + for (final IdVersionAclTriplet triplet : resultSet) { + + if (triplet.id >= 100) { + filtered.add(triplet); + } + } + return filtered; + } + /** * Cache a query result. * * @param key * @param resultSet */ - private void setCache(final String key, final List<IdVersionPair> resultSet) { + private void cacheItem(final String key, final List<IdVersionAclTriplet> resultSet) { synchronized (cache) { if (resultSet instanceof Serializable) { cache.put(key, (Serializable) resultSet); @@ -737,17 +806,17 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac } /** - * Retrieve a result set of entity ids (and the version) from the cache. + * Retrieve a result set of entity IDs (and the version) from the cache. * * @param key * @return */ @SuppressWarnings("unchecked") - private List<IdVersionPair> getCached(final String key) { - return (List<IdVersionPair>) cache.get(key); + private List<IdVersionAclTriplet> getCached(final String key) { + return (List<IdVersionAclTriplet>) cache.get(key); } - protected void executeNoCache(final Access access) { + protected void executeQueryInBackend(final Access access) throws SQLException { try { this.resultSet = getResultSet(executeStrategy(this.versioned), this.versioned); } finally { @@ -787,72 +856,100 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac /** * Filter out all entities which may not be retrieved by this user due to a missing RETRIEVE - * permission. This one is also designed for filtering of intermediate results. + * permission. This function is also designed for filtering of intermediate results. * - * @param resultSet + * @param tabname * @throws SQLException - * @throws TransactionException */ - public void filterEntitiesWithoutRetrievePermission(final String resultSet) - throws SQLException, TransactionException { + public void filterIntermediateResult(final String tabname) throws SQLException { if (!filterEntitiesWithoutRetrievePermisions) { return; } - cachable = false; + filteredIntermediateResult = true; + + /* + * The following creates a table with the columns (entity ID, acl) from + * a given table with entity IDs. Here, acl is the string representation + * of the acl. + * + * TODO:In future, one might want to retrieve only a distinct set of acl + * with (acl_id, acl) and a table with (entity_id, acl_id) to reduce the + * amount of data being transfered. + */ + try (final Statement stmt = this.getConnection().createStatement()) { - final ResultSet rs = stmt.executeQuery("SELECT id from `" + resultSet + "`"); - final List<Integer> toBeDeleted = new LinkedList<Integer>(); - while (rs.next()) { - final long t1 = System.currentTimeMillis(); - final Integer id = rs.getInt("id"); - if (id > 99 - && !execute(new RetrieveSparseEntity(id, null), this.getAccess()) - .getEntity() - .getEntityACL() - .isPermitted(this.getUser(), EntityPermission.RETRIEVE_ENTITY)) { - toBeDeleted.add(id); + final String query = + ("SELECT entity_n_acl.id, entity_acl.acl from " + + "(select entities.id, entities.acl from entities " + + "inner join `" + + tabname + + "` as rs on entities.id=rs.id) " + + "as entity_n_acl " + + "left join entity_acl on entity_n_acl.acl=entity_acl.id;"); + final ResultSet entitiesRS = stmt.executeQuery(query); + final ResultSetIterator entitiesWithACL = new ResultSetIterator(entitiesRS); + final List<Integer> toBeDeleted = collectIdsWithoutPermission(entitiesWithACL); + try (final PreparedStatement pstmt = + this.getConnection().prepareStatement("DELETE FROM `" + tabname + "` WHERE id = ?")) { + for (final Integer id : toBeDeleted) { + pstmt.setInt(1, id); + pstmt.execute(); } - final long t2 = System.currentTimeMillis(); - this.addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); - } - rs.close(); - for (final Integer id : toBeDeleted) { - stmt.execute("DELETE FROM `" + resultSet + "` WHERE id = " + id); } } } /** - * Filter out all entities which may not be retrieved by this user due to a missing RETRIEVE - * permission. This one is for the filtering of the final result set and not for the filtering of - * any intermediate results. + * Creates a new list that contains only the entities from the `resultSet` for which the current + * subject has RETRIEVE permission. * - * @param entities - * @throws TransactionException - * @return the filtered list. + * <p>Note, unlike the public version of this function `resultSet` is not altered but the filtered + * list is returned. + * + * @param resultSet + * @return list without the entities with insufficient permissions */ - private List<IdVersionPair> filterEntitiesWithoutRetrievePermission( - final List<IdVersionPair> entities) throws TransactionException { - if (!filterEntitiesWithoutRetrievePermisions) { - return entities; + private List<IdVersionAclTriplet> filterEntitiesWithoutRetrievePermission( + final List<IdVersionAclTriplet> resultSet) { + final List<Integer> toBeDeleted = collectIdsWithoutPermission(resultSet.iterator()); + final List<IdVersionAclTriplet> filtered = new ArrayList<>(); + for (final IdVersionAclTriplet triplet : resultSet) { + if (-1 == toBeDeleted.indexOf(triplet.id)) { + filtered.add(triplet); + } } - - final List<IdVersionPair> result = new ArrayList<>(); - final Iterator<IdVersionPair> iterator = entities.iterator(); - while (iterator.hasNext()) { + return filtered; + } + /** + * Creates a list with IDs of those entities that do not have sufficient RETRIEVE permission + * + * @param entityIterator Iterator over the result set consisting of (ID, version hash, acl string) + * triplets. + * @return compiled list + */ + private List<Integer> collectIdsWithoutPermission(Iterator<IdVersionAclTriplet> entityIterator) { + final HashMap<String, Boolean> acl_cache = new HashMap<String, Boolean>(); + final List<Integer> toBeDeleted = new LinkedList<Integer>(); + while (entityIterator.hasNext()) { final long t1 = System.currentTimeMillis(); - final IdVersionPair next = iterator.next(); - if (next.id > 99 - && execute(new RetrieveSparseEntity(next.id, next.version), getAccess()) - .getEntity() - .getEntityACL() - .isPermitted(getUser(), EntityPermission.RETRIEVE_ENTITY)) { - result.add(next); + + final IdVersionAclTriplet triplet = entityIterator.next(); + + if (!acl_cache.containsKey(triplet.acl)) { + acl_cache.put( + triplet.acl, + EntityACL.deserialize(triplet.acl) + .isPermitted(this.getUser(), EntityPermission.RETRIEVE_ENTITY)); } + + if (!acl_cache.get(triplet.acl)) { + toBeDeleted.add(triplet.id); + } + final long t2 = System.currentTimeMillis(); - addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); + this.addBenchmark("filterEntitiesWithoutRetrievePermission", t2 - t1); } - return result; + return toBeDeleted; } @Override @@ -1004,18 +1101,29 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac * * @return A Cache key. */ - String getCacheKey() { + String getCacheKey(boolean addUser) { final StringBuilder sb = new StringBuilder(); + if (addUser && (this.user != null)) { + sb.append("U_"); + String principal_desc = + ((Principal) this.user.getPrincipal()).getUsername() + + Principal.REALM_SEPARATOR + + ((Principal) this.user.getPrincipal()).getRealm(); + sb.append(principal_desc); + } if (this.versioned) { - sb.append("versioned"); + sb.append("V_"); } if (this.role != null) { + sb.append("R_"); sb.append(this.role.toString()); } if (this.entity != null) { + sb.append("E_"); sb.append(this.entity.toString()); } if (this.filter != null) { + sb.append("F_"); sb.append(this.filter.getCacheKey()); } return sb.toString(); diff --git a/src/main/java/org/caosdb/server/query/SubProperty.java b/src/main/java/org/caosdb/server/query/SubProperty.java index f3be825ccf28b7df2ba646306a61f006664ae5a6..8ff167eac1e045875576a75f3d922f5c6461c68e 100644 --- a/src/main/java/org/caosdb/server/query/SubProperty.java +++ b/src/main/java/org/caosdb/server/query/SubProperty.java @@ -87,7 +87,7 @@ public class SubProperty implements QueryInterface, EntityFilterInterface { this.filter.apply(this); - getQuery().filterEntitiesWithoutRetrievePermission(this.sourceSet); + getQuery().filterIntermediateResult(this.sourceSet); final CallableStatement callFinishSubProperty = getConnection().prepareCall("call finishSubProperty(?,?,?,?)"); diff --git a/src/main/java/org/caosdb/server/utils/ResultSetIterator.java b/src/main/java/org/caosdb/server/utils/ResultSetIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..0912d278c7158c9b7b75a585c2827d851951f29b --- /dev/null +++ b/src/main/java/org/caosdb/server/utils/ResultSetIterator.java @@ -0,0 +1,60 @@ +package org.caosdb.server.utils; + +import static org.caosdb.server.database.DatabaseUtils.bytes2UTF8; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.NoSuchElementException; +import org.caosdb.server.database.exceptions.TransactionException; +import org.caosdb.server.query.Query.IdVersionAclTriplet; + +/** + * A class for iterating over {@link ResultSet} + * + * <p>{@link ResultSet} only provides a `next` function which moves the cursor. The behavior is here + * mapped onto the functions of the Iterator interface. TODO Move this generic function? Check again + * if an implementation is available from elsewhere. + */ +public class ResultSetIterator implements Iterator<IdVersionAclTriplet> { + public ResultSetIterator(final ResultSet resultset) { + this.resultSet = resultset; + } + + private ResultSet resultSet; + private boolean cursorHasMoved = false; + private boolean currentIsValid = true; + + public boolean hasNext() { + if (!this.cursorHasMoved) { + try { + this.currentIsValid = this.resultSet.next(); + } catch (SQLException e) { + throw new TransactionException(e); + } + this.cursorHasMoved = true; + } + return this.currentIsValid; + }; + + public IdVersionAclTriplet next() { + if (!this.cursorHasMoved) { + try { + this.currentIsValid = this.resultSet.next(); + } catch (SQLException e) { + throw new TransactionException(e); + } + } + this.cursorHasMoved = false; + if (!this.currentIsValid) { + throw new NoSuchElementException(); + } + try { + final Integer id = resultSet.getInt("id"); + final String acl_str = bytes2UTF8(resultSet.getBytes("ACL")); + return new IdVersionAclTriplet(id, "", acl_str); + } catch (SQLException e) { + throw new TransactionException(e); + } + } +} diff --git a/src/test/java/org/caosdb/server/query/QueryTest.java b/src/test/java/org/caosdb/server/query/QueryTest.java index 8f3d4946c36d795e5e7bd9048a66f86cc0290a0b..a039f2106ab8f1a0c6ba61bdd906ba86372c9281 100644 --- a/src/test/java/org/caosdb/server/query/QueryTest.java +++ b/src/test/java/org/caosdb/server/query/QueryTest.java @@ -26,7 +26,11 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import java.io.IOException; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; import org.caosdb.server.CaosDBServer; +import org.caosdb.server.ServerProperties; +import org.caosdb.server.accessControl.AnonymousAuthenticationToken; import org.caosdb.server.database.access.InitAccess; import org.caosdb.server.transaction.WriteTransaction; import org.junit.BeforeClass; @@ -37,41 +41,57 @@ public class QueryTest { @BeforeClass public static void initServerProperties() throws IOException { CaosDBServer.initServerProperties(); + CaosDBServer.initShiro(); } String getCacheKey(String query) { Query q = new Query(query); q.parse(); - return q.getCacheKey(); + return q.getCacheKey(true); + } + + String getCacheKeyWithUser(String query) { + Subject anonymous = SecurityUtils.getSubject(); + CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true"); + anonymous.login(AnonymousAuthenticationToken.getInstance()); + Query q = new Query(query, anonymous); + q.parse(); + return q.getCacheKey(true); } @Test public void testGetKey() { - assertEquals("enamePOV(pname,=,val1)", getCacheKey("FIND ename WITH pname = val1")); - assertEquals("enamePOV(pname,=,val1)", getCacheKey("COUNT ename WITH pname = val1")); - assertEquals("enamePOV(pname,=,val1)", getCacheKey("SELECT bla FROM ename WITH pname = val1")); - assertEquals("enamePOV(pname,null,null)", getCacheKey("SELECT bla FROM ename WITH pname")); + assertEquals("E_enameF_POV(pname,=,val1)", getCacheKey("FIND ename WITH pname = val1")); + assertEquals("E_enameF_POV(pname,=,val1)", getCacheKey("COUNT ename WITH pname = val1")); + assertEquals( + "E_enameF_POV(pname,=,val1)", getCacheKey("SELECT bla FROM ename WITH pname = val1")); + assertEquals("E_enameF_POV(pname,null,null)", getCacheKey("SELECT bla FROM ename WITH pname")); assertEquals( - "enamemaxPOV(pname,null,null)", + "E_enameF_maxPOV(pname,null,null)", getCacheKey("SELECT bla FROM ename WITH THE GREATEST pname")); assertEquals( - "RECORDenamePOV(pname,=,val1)", getCacheKey("FIND RECORD ename WITH pname = val1")); - assertEquals("ENTITYPOV(pname,=,val1)", getCacheKey("COUNT ENTITY WITH pname = val1")); + "R_RECORDE_enameF_POV(pname,=,val1)", getCacheKey("FIND RECORD ename WITH pname = val1")); + assertEquals("R_ENTITYF_POV(pname,=,val1)", getCacheKey("COUNT ENTITY WITH pname = val1")); assertEquals( - "enameConj(POV(pname,=,val1)POV(ename2,=,val2))", + "E_enameF_Conj(POV(pname,=,val1)POV(ename2,=,val2))", getCacheKey("SELECT bla FROM ename WITH pname = val1 AND ename2 = val2")); - assertEquals("versionedENTITYID(,>,2)", getCacheKey("FIND ANY VERSION OF ENTITY WITH ID > 2")); - assertEquals("ENTITYID(min,,)", getCacheKey("FIND ENTITY WITH THE SMALLEST ID")); - assertEquals("ENTITYSAT(asdf/%%)", getCacheKey("FIND ENTITY WHICH IS STORED AT /asdf/*")); - assertEquals("ENTITYSAT(asdf/asdf)", getCacheKey("FIND ENTITY WHICH IS STORED AT asdf/asdf")); + assertEquals("V_R_ENTITYF_ID(,>,2)", getCacheKey("FIND ANY VERSION OF ENTITY WITH ID > 2")); + assertEquals("R_ENTITYF_ID(min,,)", getCacheKey("FIND ENTITY WITH THE SMALLEST ID")); + assertEquals("R_ENTITYF_SAT(asdf/%%)", getCacheKey("FIND ENTITY WHICH IS STORED AT /asdf/*")); assertEquals( - "enamePOV(ref1,null,null)SUB(POV(pname,>,val1)", + "R_ENTITYF_SAT(asdf/asdf)", getCacheKey("FIND ENTITY WHICH IS STORED AT asdf/asdf")); + assertEquals( + "E_enameF_POV(ref1,null,null)SUB(POV(pname,>,val1)", getCacheKey("FIND ename WITH ref1 WITH pname > val1 ")); assertEquals( - "ename@(ref1,null)SUB(POV(pname,>,val1)", + "E_enameF_@(ref1,null)SUB(POV(pname,>,val1)", getCacheKey("FIND ename WHICH IS REFERENCED BY ref1 WITH pname > val1 ")); + + assertEquals( + "U_anonymous@anonymousE_enameF_POV(pname,=,val1)", + getCacheKeyWithUser("FIND ename WITH pname = val1")); } @Test