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    }