001 /* 002 * JBoss DNA (http://www.jboss.org/dna) 003 * See the COPYRIGHT.txt file distributed with this work for information 004 * regarding copyright ownership. Some portions may be licensed 005 * to Red Hat, Inc. under one or more contributor license agreements. 006 * See the AUTHORS.txt file in the distribution for a full listing of 007 * individual contributors. 008 * 009 * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA 010 * is licensed to you under the terms of the GNU Lesser General Public License as 011 * published by the Free Software Foundation; either version 2.1 of 012 * the License, or (at your option) any later version. 013 * 014 * JBoss DNA is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 017 * Lesser General Public License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this software; if not, write to the Free 021 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 022 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 023 */ 024 package org.jboss.dna.connector.store.jpa.model.basic; 025 026 import java.util.HashMap; 027 import java.util.LinkedList; 028 import java.util.List; 029 import java.util.Map; 030 import java.util.UUID; 031 import javax.persistence.EntityManager; 032 import javax.persistence.NoResultException; 033 import javax.persistence.Query; 034 import org.jboss.dna.graph.ExecutionContext; 035 import org.jboss.dna.graph.Location; 036 import org.jboss.dna.graph.property.Name; 037 import org.jboss.dna.graph.property.NameFactory; 038 import org.jboss.dna.graph.property.Path; 039 import org.jboss.dna.graph.property.PathFactory; 040 041 /** 042 * Represents a temporary working area for a query that efficiently retrieves the nodes in a subgraph. This class uses the 043 * database to build up the content of the subgraph, and therefore requires write privilege on the database. The benefit is that 044 * it minimizes the amount of memory required to process the subgraph, plus the set of nodes that make up the subgraph can be 045 * produced with database joins. 046 * <p> 047 * The use of database joins also produces another benefit: the number of SQL statements necessary to build the set of nodes in a 048 * subgraph is equal to the depth of the subgraph, regardless of the number of child nodes at any level. 049 * </p> 050 * 051 * @author Randall Hauch 052 */ 053 public class SubgraphQuery { 054 055 /** 056 * Create a query that returns a subgraph at and below the node with the supplied path and the supplied UUID. 057 * 058 * @param context the execution context; may not be null 059 * @param entities the entity manager; may not be null 060 * @param workspaceId the ID of the workspace; may not be null 061 * @param subgraphRootUuid the UUID (in string form) of the root node in the subgraph 062 * @param subgraphRootPath the path of the root node in the subgraph 063 * @param maxDepth the maximum depth of the subgraph, or 0 if there is no maximum depth 064 * @return the object representing the subgraph 065 */ 066 public static SubgraphQuery create( ExecutionContext context, 067 EntityManager entities, 068 Long workspaceId, 069 UUID subgraphRootUuid, 070 Path subgraphRootPath, 071 int maxDepth ) { 072 assert entities != null; 073 assert subgraphRootUuid != null; 074 assert workspaceId != null; 075 assert maxDepth >= 0; 076 if (maxDepth == 0) maxDepth = Integer.MAX_VALUE; 077 final String subgraphRootUuidString = subgraphRootUuid.toString(); 078 // Create a new subgraph query, and add a child for the root ... 079 SubgraphQueryEntity query = new SubgraphQueryEntity(workspaceId, subgraphRootUuidString); 080 entities.persist(query); 081 Long queryId = query.getId(); 082 083 try { 084 // Insert a node for the root (this will be the starting point for the recursive operation) ... 085 SubgraphNodeEntity root = new SubgraphNodeEntity(queryId, subgraphRootUuidString, 0); 086 entities.persist(root); 087 088 // Now add the children by inserting the children, one level at a time. 089 // Note that we do this for the root, and for each level until 1 BEYOND 090 // the max depth (so that we can get the children for the nodes that are 091 // at the maximum depth)... 092 Query statement = entities.createNamedQuery("SubgraphNodeEntity.insertChildren"); 093 int numChildrenInserted = 0; 094 int parentLevel = 0; 095 while (parentLevel <= maxDepth) { 096 // Insert the children of the next level by inserting via a select (join) of the children 097 statement.setParameter("queryId", queryId); 098 statement.setParameter("workspaceId", workspaceId); 099 statement.setParameter("parentDepth", parentLevel); 100 numChildrenInserted = statement.executeUpdate(); 101 if (numChildrenInserted == 0) break; 102 parentLevel = parentLevel + 1; 103 } 104 } catch (RuntimeException t) { 105 // Clean up the search and results ... 106 try { 107 Query search = entities.createNamedQuery("SubgraphNodeEntity.deleteByQueryId"); 108 search.setParameter("queryId", query.getId()); 109 search.executeUpdate(); 110 } finally { 111 entities.remove(query); 112 } 113 throw t; 114 } 115 return new SubgraphQuery(context, entities, workspaceId, query, subgraphRootPath, maxDepth); 116 } 117 118 private final ExecutionContext context; 119 private final EntityManager manager; 120 private final Long workspaceId; 121 private SubgraphQueryEntity query; 122 private final int maxDepth; 123 private final Path subgraphRootPath; 124 125 protected SubgraphQuery( ExecutionContext context, 126 EntityManager manager, 127 Long workspaceId, 128 SubgraphQueryEntity query, 129 Path subgraphRootPath, 130 int maxDepth ) { 131 assert manager != null; 132 assert query != null; 133 assert context != null; 134 assert subgraphRootPath != null; 135 assert workspaceId != null; 136 this.context = context; 137 this.manager = manager; 138 this.workspaceId = workspaceId; 139 this.query = query; 140 this.maxDepth = maxDepth; 141 this.subgraphRootPath = subgraphRootPath; 142 } 143 144 /** 145 * @return maxDepth 146 */ 147 public int getMaxDepth() { 148 return maxDepth; 149 } 150 151 /** 152 * @return manager 153 */ 154 public EntityManager getEntityManager() { 155 return manager; 156 } 157 158 /** 159 * @return subgraphRootPath 160 */ 161 public Path getSubgraphRootPath() { 162 return subgraphRootPath; 163 } 164 165 /** 166 * @return query 167 */ 168 public SubgraphQueryEntity getSubgraphQueryEntity() { 169 if (query == null) throw new IllegalStateException(); 170 return query; 171 } 172 173 public int getNodeCount( boolean includeRoot ) { 174 if (query == null) throw new IllegalStateException(); 175 // Now query for all the nodes and put into a list ... 176 Query search = manager.createNamedQuery("SubgraphNodeEntity.getCount"); 177 search.setParameter("queryId", query.getId()); 178 179 // Now process the nodes below the subgraph's root ... 180 try { 181 return (Integer)search.getSingleResult() - (includeRoot ? 0 : 1); 182 } catch (NoResultException e) { 183 return 0; 184 } 185 } 186 187 /** 188 * Get the {@link ChildEntity root node} of the subgraph. This must be called before the query is {@link #close() closed}. 189 * 190 * @return the subgraph's root nodes 191 */ 192 public ChildEntity getNode() { 193 // Now query for all the nodes and put into a list ... 194 Query search = manager.createNamedQuery("SubgraphNodeEntity.getChildEntities"); 195 search.setParameter("queryId", query.getId()); 196 search.setParameter("workspaceId", workspaceId); 197 search.setParameter("depth", 0); 198 search.setParameter("maxDepth", 0); 199 200 // Now process the nodes below the subgraph's root ... 201 return (ChildEntity)search.getSingleResult(); 202 } 203 204 /** 205 * Get the {@link ChildEntity nodes} in the subgraph. This must be called before the query is {@link #close() closed}. 206 * 207 * @param includeRoot true if the subgraph's root node is to be included, or false otherwise 208 * @param includeChildrenOfMaxDepthNodes true if the method is to include nodes that are children of nodes that are at the 209 * maximum depth, or false if only nodes up to the maximum depth are to be included 210 * @return the list of nodes, in breadth-first order 211 */ 212 @SuppressWarnings( "unchecked" ) 213 public List<ChildEntity> getNodes( boolean includeRoot, 214 boolean includeChildrenOfMaxDepthNodes ) { 215 if (query == null) throw new IllegalStateException(); 216 // Now query for all the nodes and put into a list ... 217 Query search = manager.createNamedQuery("SubgraphNodeEntity.getChildEntities"); 218 search.setParameter("queryId", query.getId()); 219 search.setParameter("workspaceId", workspaceId); 220 search.setParameter("depth", includeRoot ? 0 : 1); 221 search.setParameter("maxDepth", includeChildrenOfMaxDepthNodes ? maxDepth : maxDepth - 1); 222 223 // Now process the nodes below the subgraph's root ... 224 return search.getResultList(); 225 } 226 227 /** 228 * Get the {@link PropertiesEntity properties} for each of the nodes in the subgraph. This must be called before the query is 229 * {@link #close() closed}. 230 * 231 * @param includeRoot true if the properties for the subgraph's root node are to be included, or false otherwise 232 * @param includeChildrenOfMaxDepthNodes true if the method is to include nodes that are children of nodes that are at the 233 * maximum depth, or false if only nodes up to the maximum depth are to be included 234 * @return the list of properties for each of the nodes, in breadth-first order 235 */ 236 @SuppressWarnings( "unchecked" ) 237 public List<PropertiesEntity> getProperties( boolean includeRoot, 238 boolean includeChildrenOfMaxDepthNodes ) { 239 if (query == null) throw new IllegalStateException(); 240 // Now query for all the nodes and put into a list ... 241 Query search = manager.createNamedQuery("SubgraphNodeEntity.getPropertiesEntities"); 242 search.setParameter("queryId", query.getId()); 243 search.setParameter("workspaceId", workspaceId); 244 search.setParameter("depth", includeRoot ? 0 : 1); 245 search.setParameter("maxDepth", includeChildrenOfMaxDepthNodes ? maxDepth : maxDepth - 1); 246 247 // Now process the nodes below the subgraph's root ... 248 return search.getResultList(); 249 } 250 251 /** 252 * Get the {@link Location} for each of the nodes in the subgraph. This must be called before the query is {@link #close() 253 * closed}. 254 * <p> 255 * This method calls {@link #getNodes(boolean,boolean)}. Therefore, calling {@link #getNodes(boolean,boolean)} and this method 256 * for the same subgraph is not efficient; consider just calling {@link #getNodes(boolean,boolean)} alone. 257 * </p> 258 * 259 * @param includeRoot true if the properties for the subgraph's root node are to be included, or false otherwise 260 * @param includeChildrenOfMaxDepthNodes true if the method is to include nodes that are children of nodes that are at the 261 * maximum depth, or false if only nodes up to the maximum depth are to be included 262 * @return the list of {@link Location locations}, one for each of the nodes in the subgraph, in breadth-first order 263 */ 264 public List<Location> getNodeLocations( boolean includeRoot, 265 boolean includeChildrenOfMaxDepthNodes ) { 266 if (query == null) throw new IllegalStateException(); 267 // Set up a map of the paths to the nodes, keyed by UUIDs. This saves us from having to build 268 // the paths every time ... 269 Map<String, Path> pathByUuid = new HashMap<String, Path>(); 270 LinkedList<Location> locations = new LinkedList<Location>(); 271 String subgraphRootUuid = query.getRootUuid(); 272 pathByUuid.put(subgraphRootUuid, subgraphRootPath); 273 UUID uuid = UUID.fromString(subgraphRootUuid); 274 if (includeRoot) { 275 locations.add(Location.create(subgraphRootPath, uuid)); 276 } 277 278 // Now iterate over the child nodes in the subgraph (we've already included the root) ... 279 final PathFactory pathFactory = context.getValueFactories().getPathFactory(); 280 final NameFactory nameFactory = context.getValueFactories().getNameFactory(); 281 for (ChildEntity entity : getNodes(false, includeChildrenOfMaxDepthNodes)) { 282 String parentUuid = entity.getId().getParentUuidString(); 283 Path parentPath = pathByUuid.get(parentUuid); 284 assert parentPath != null; 285 String nsUri = entity.getChildNamespace().getUri(); 286 String localName = entity.getChildName(); 287 int sns = entity.getSameNameSiblingIndex(); 288 Name childName = nameFactory.create(nsUri, localName); 289 Path childPath = pathFactory.create(parentPath, childName, sns); 290 String childUuid = entity.getId().getChildUuidString(); 291 pathByUuid.put(childUuid, childPath); 292 uuid = UUID.fromString(childUuid); 293 locations.add(Location.create(childPath, uuid)); 294 295 } 296 return locations; 297 } 298 299 /** 300 * Get the list of references that are owned by nodes within the subgraph and that point to other nodes <i>in this same 301 * subgraph</i>. This set of references is important in copying a subgraph, since all intra-subgraph references in the 302 * original subgraph must also be intra-subgraph references in the copy. 303 * 304 * @return the list of references completely contained by this subgraphs 305 */ 306 @SuppressWarnings( "unchecked" ) 307 public List<ReferenceEntity> getInternalReferences() { 308 Query references = manager.createNamedQuery("SubgraphNodeEntity.getInternalReferences"); 309 references.setParameter("queryId", query.getId()); 310 references.setParameter("workspaceId", workspaceId); 311 return references.getResultList(); 312 } 313 314 /** 315 * Get the list of references that are owned by nodes within the subgraph and that point to nodes <i>not in this same 316 * subgraph</i>. This set of references is important in copying a subgraph. 317 * 318 * @return the list of references that are owned by the subgraph but that point to nodes outside of the subgraph 319 */ 320 @SuppressWarnings( "unchecked" ) 321 public List<ReferenceEntity> getOutwardReferences() { 322 Query references = manager.createNamedQuery("SubgraphNodeEntity.getOutwardReferences"); 323 references.setParameter("queryId", query.getId()); 324 references.setParameter("workspaceId", workspaceId); 325 return references.getResultList(); 326 } 327 328 /** 329 * Get the list of references that are owned by nodes <i>outside</i> of the subgraph that point to nodes <i>in this 330 * subgraph</i>. This set of references is important in deleting nodes, since such references prevent the deletion of the 331 * subgraph. 332 * 333 * @return the list of references that are no longer valid 334 */ 335 @SuppressWarnings( "unchecked" ) 336 public List<ReferenceEntity> getInwardReferences() { 337 // Verify referential integrity: that none of the deleted nodes are referenced by nodes not being deleted. 338 Query references = manager.createNamedQuery("SubgraphNodeEntity.getInwardReferences"); 339 references.setParameter("queryId", query.getId()); 340 references.setParameter("workspaceId", workspaceId); 341 return references.getResultList(); 342 } 343 344 /** 345 * Delete the nodes in the subgraph. This method first does not check for referential integrity (see 346 * {@link #getInwardReferences()}). 347 * 348 * @param includeRoot true if the root node should also be deleted 349 */ 350 @SuppressWarnings( "unchecked" ) 351 public void deleteSubgraph( boolean includeRoot ) { 352 if (query == null) throw new IllegalStateException(); 353 354 // Delete the PropertiesEntities ... 355 // 356 // Right now, Hibernate is not able to support deleting PropertiesEntity in bulk because of the 357 // large value association (and there's no way to clear the association in bulk). 358 // Therefore, the only way to do this with Hibernate is to load each PropertiesEntity that has 359 // large values and clear them. (Theoretically, fewer PropertiesEntity objects will have large values 360 // than the total number in the subgraph.) 361 // Then we can delete the properties. 362 Query withLargeValues = manager.createNamedQuery("SubgraphNodeEntity.getPropertiesEntitiesWithLargeValues"); 363 withLargeValues.setParameter("queryId", query.getId()); 364 withLargeValues.setParameter("depth", includeRoot ? 0 : 1); 365 withLargeValues.setParameter("workspaceId", workspaceId); 366 List<PropertiesEntity> propertiesWithLargeValues = withLargeValues.getResultList(); 367 if (propertiesWithLargeValues.size() != 0) { 368 for (PropertiesEntity props : propertiesWithLargeValues) { 369 props.getLargeValues().clear(); 370 } 371 manager.flush(); 372 } 373 374 // Delete the PropertiesEntities, none of which will have large values ... 375 Query delete = manager.createNamedQuery("SubgraphNodeEntity.deletePropertiesEntities"); 376 delete.setParameter("queryId", query.getId()); 377 delete.setParameter("workspaceId", workspaceId); 378 delete.executeUpdate(); 379 380 // Delete the ChildEntities ... 381 delete = manager.createNamedQuery("SubgraphNodeEntity.deleteChildEntities"); 382 delete.setParameter("queryId", query.getId()); 383 delete.setParameter("workspaceId", workspaceId); 384 delete.executeUpdate(); 385 386 // Delete references ... 387 delete = manager.createNamedQuery("SubgraphNodeEntity.deleteReferences"); 388 delete.setParameter("queryId", query.getId()); 389 delete.setParameter("workspaceId", workspaceId); 390 delete.executeUpdate(); 391 392 // Delete unused large values ... 393 LargeValueEntity.deleteUnused(manager); 394 395 manager.flush(); 396 } 397 398 /** 399 * Close this query object and clean up all in-database records associated with this query. This method <i>must</i> be called 400 * when this query is no longer needed, and once it is called, this subgraph query is no longer usable. 401 */ 402 public void close() { 403 if (query == null) return; 404 // Clean up the search and results ... 405 try { 406 Query search = manager.createNamedQuery("SubgraphNodeEntity.deleteByQueryId"); 407 search.setParameter("queryId", query.getId()); 408 search.executeUpdate(); 409 } finally { 410 try { 411 manager.remove(query); 412 } finally { 413 query = null; 414 } 415 } 416 } 417 }