View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.connector.store.jpa.model.basic;
25  
26  import java.util.HashMap;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.UUID;
31  import javax.persistence.EntityManager;
32  import javax.persistence.NoResultException;
33  import javax.persistence.Query;
34  import org.modeshape.graph.ExecutionContext;
35  import org.modeshape.graph.Location;
36  import org.modeshape.graph.property.Name;
37  import org.modeshape.graph.property.NameFactory;
38  import org.modeshape.graph.property.Path;
39  import org.modeshape.graph.property.PathFactory;
40  
41  /**
42   * Represents a temporary working area for a query that efficiently retrieves the nodes in a subgraph. This class uses the
43   * database to build up the content of the subgraph, and therefore requires write privilege on the database. The benefit is that
44   * it minimizes the amount of memory required to process the subgraph, plus the set of nodes that make up the subgraph can be
45   * produced with database joins.
46   * <p>
47   * The use of database joins also produces another benefit: the number of SQL statements necessary to build the set of nodes in a
48   * subgraph is equal to the depth of the subgraph, regardless of the number of child nodes at any level.
49   * </p>
50   */
51  public class SubgraphQuery {
52  
53      /**
54       * Create a query that returns a subgraph at and below the node with the supplied path and the supplied UUID.
55       * 
56       * @param context the execution context; may not be null
57       * @param entities the entity manager; may not be null
58       * @param workspaceId the ID of the workspace; may not be null
59       * @param subgraphRootUuid the UUID (in string form) of the root node in the subgraph
60       * @param subgraphRootPath the path of the root node in the subgraph
61       * @param maxDepth the maximum depth of the subgraph, or 0 if there is no maximum depth
62       * @return the object representing the subgraph
63       */
64      public static SubgraphQuery create( ExecutionContext context,
65                                          EntityManager entities,
66                                          Long workspaceId,
67                                          UUID subgraphRootUuid,
68                                          Path subgraphRootPath,
69                                          int maxDepth ) {
70          assert entities != null;
71          assert subgraphRootUuid != null;
72          assert workspaceId != null;
73          assert maxDepth >= 0;
74  
75          if (maxDepth == 0) maxDepth = Integer.MAX_VALUE;
76          final String subgraphRootUuidString = subgraphRootUuid.toString();
77          // Create a new subgraph query, and add a child for the root ...
78          SubgraphQueryEntity query = new SubgraphQueryEntity(workspaceId, subgraphRootUuidString);
79          entities.persist(query);
80          Long queryId = query.getId();
81  
82          try {
83              // Insert a node for the root (this will be the starting point for the recursive operation) ...
84              SubgraphNodeEntity root = new SubgraphNodeEntity(queryId, subgraphRootUuidString, 0);
85              entities.persist(root);
86  
87              // Now add the children by inserting the children, one level at a time.
88              // Note that we do this for the root, and for each level until 1 BEYOND
89              // the max depth (so that we can get the children for the nodes that are
90              // at the maximum depth)...
91              Query statement = entities.createNamedQuery("SubgraphNodeEntity.insertChildren");
92              int numChildrenInserted = 0;
93              int parentLevel = 0;
94              while (parentLevel <= maxDepth) {
95                  // Insert the children of the next level by inserting via a select (join) of the children
96                  statement.setParameter("queryId", queryId);
97                  statement.setParameter("workspaceId", workspaceId);
98                  statement.setParameter("parentDepth", parentLevel);
99                  numChildrenInserted = statement.executeUpdate();
100                 if (numChildrenInserted == 0) break;
101                 parentLevel = parentLevel + 1;
102             }
103         } catch (RuntimeException t) {
104             // Clean up the search and results ...
105             try {
106                 Query search = entities.createNamedQuery("SubgraphNodeEntity.deleteByQueryId");
107                 search.setParameter("queryId", query.getId());
108                 search.executeUpdate();
109             } finally {
110                 entities.remove(query);
111             }
112             throw t;
113         }
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.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 }