head	1.7;
access;
symbols;
locks; strict;
comment	@# @;


1.7
date	2001.05.30.01.11.12;	author leonardr;	state dead;
branches;
next	1.6;

1.6
date	2001.05.23.00.35.00;	author leonardr;	state Exp;
branches;
next	1.5;

1.5
date	2001.05.22.18.52.57;	author leonardr;	state Exp;
branches;
next	1.4;

1.4
date	2001.05.22.00.14.19;	author leonardr;	state Exp;
branches;
next	1.3;

1.3
date	2001.05.21.21.06.01;	author leonardr;	state Exp;
branches;
next	1.2;

1.2
date	2001.05.17.14.51.17;	author leonardr;	state Exp;
branches;
next	1.1;

1.1
date	2001.05.11.19.38.55;	author leonardr;	state Exp;
branches;
next	;


desc
@@


1.7
log
@Got rid of denormalized project-project group mapping table in favor of per-JVM caching solution.
@
text
@/* ================================================================
 * Copyright (c) 2001 CollabNet.  All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * 
 * 3. The end-user documentation included with the redistribution, if
 * any, must include the following acknowlegement: "This product includes
 * software developed by CollabNet (http://www.collab.net/)."
 * Alternately, this acknowlegement may appear in the software itself, if
 * and wherever such third-party acknowlegements normally appear.
 * 
 * 4. The hosted project names must not be used to endorse or promote
 * products derived from this software without prior written
 * permission. For written permission, please contact info@@collab.net.
 * 
 * 5. Products derived from this software may not use the "Tigris" name
 * nor may "Tigris" appear in their names without prior written
 * permission of CollabNet.
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL COLLABNET OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ====================================================================
 * 
 * This software consists of voluntary contributions made by many
 * individuals on behalf of CollabNet.
 */

package org.tigris.helm.tool.acl;

import java.util.*;
import java.rmi.RemoteException;

import org.apache.turbine.om.*;
import org.apache.turbine.util.db.Criteria;
import org.apache.turbine.util.db.pool.DBConnection;
import org.apache.turbine.util.Log;

import org.tigris.helm.om.*;
import org.tigris.helm.tool.cache.RegisterableCache;

/** 
 * A utility class to respond to cache invalidation events by
 * denormalizing the project group mapping table into another table
 * intended for this purpose.
 *
 * @@author <a href="mailto:leonardr@@collab.net">Leonard Richardson</a> 
 */
public class ProjectGraphDenormalizer extends RegisterableCache
{

    /**
     * Whether or not to log debugging information.
     */
    public static final boolean DEBUG = true;

    /**
     * Whether or not to log timing information.
     */
    public static final boolean TIMING_DEBUG = true;

    /**
     * This is used to detect circular references. While processing a
     * project's children, it gets put into this set. If a project in
     * the set turns out to be referenced lower in the class tree, an
     * error is thrown because that means a circular reference to that
     * project exists.  
     */
    private Set circularReferenceDetector = new HashSet();

    /**
     * This is used to save work, so that the same project doesn't get
     * its descendants noted twice if it gets encountered twice in the
     * graph. 
     */
    private Set alreadyProcessedProjects = new HashSet();

    /**
     * Keeps track of mappings of parent projects to Lists of child
     * projects, where the child is reachable from the parent
     * through a path consisting solely of public projects; 
     */
    private Map publicMappings = new HashMap();

    /**
     * Keeps track of mappings of parent projects to child projects,
     * where the child is reachable through the parent only through
     * paths which contain one or more private projects.  
     */
    private Map privateMappings = new HashMap();

    private static Class[] interestingDescriptors = 
    {
        ProjectProjectgroup.class,
        Project.class,
        DenormalizedProjectProjectgroup.class
    };

    /**
     * Registers with this JVM's 
     * {@@link org.tigris.helm.tool.cache.CacheRegistry}.
     */
    public ProjectGraphDenormalizer()
        throws RemoteException
    {        
        super("Project graph denormalizer", interestingDescriptors);
    }

    /**
     * Invalidates some portion of the cache.
     *
     * @@param invalidationDescriptor Description of the invalidation that 
     *                               should occur, or <code>null</code> for
     *                               complete invalidation.
     */
    public void invalidate()
    {
        try
        {
            rebuildDenormalizedTable();
        }
        catch (Exception e)
        {
            throw new Error
                ("Rebuild of denormalized table failed!");
        }                    
    }

    /**
     * Completely rebuilds the table.
     */
    public synchronized void rebuildDenormalizedTable()
        throws Exception
    {
        long before = 0;
        if (TIMING_DEBUG)
        {            
            before = System.currentTimeMillis();        
        }

        //Get rid of all current entries.
        circularReferenceDetector.clear();
        alreadyProcessedProjects.clear();
        publicMappings.clear();
        privateMappings.clear();

        //Perhaps do initial check to make sure that the tree starts
        //out with only the host project, which contains only domain
        //projects.
        buildPathsFromParentToChildren(ProjectPeer.getHostProject());

        DenormalizedProjectProjectgroupPeer.replaceMappings(publicMappings,
                                                            privateMappings);

        if (TIMING_DEBUG)
        {
            Log.debug("Denormalized table rebuild took " 
                      + String.valueOf(System.currentTimeMillis()-before) 
                      + " milliseconds.");
        }
    }

    /**
     * Builds that portion of the table which represents paths from
     * the given project to all of its recursive children.
     */
    private void buildPathsFromParentToChildren(Project parent)
        throws Exception
    {
        if (alreadyProcessedProjects.contains(parent))
        {
            //We've already built all these recursive paths.
            if (DEBUG)
            {
                Log.debug("I've already processed " + parent.getName());
            }
            return;
        }
        
        if (DEBUG)
        {
            Log.debug("I'm processing " + parent.getName());
        }
        
        //
        if (circularReferenceDetector.contains(parent))
        {
            throw new Error("Circular reference in project graph! " 
                            + parent.getName() + " contains itself!");
        }

        circularReferenceDetector.add(parent);
        List directChildren = parent.getDirectChildren();
        if (DEBUG)
        {
            Log.debug("It has " + directChildren.size() + " direct children.");
        }
        for (Iterator i = directChildren.iterator(); i.hasNext();)
        {
            Project child = (Project)i.next();

            //Roles do not recurse from categories to non-categories.
            //So we disregard such paths for our purposes.
            if (parent.isCategory() && !child.isCategory() )
            {
                if (DEBUG)
                {
                    Log.debug(parent.getName() 
                              + " is a category, not processing"
                              + " non-category child " + child.getName());
                }
            }
            else
            {
                if (DEBUG)
                {
                    Log.debug("Recursively calling on " + child.getName());
                }
                buildPathsFromParentToChildren(child);
                if (DEBUG)
                {
                    Log.debug("Done with call to " + child.getName() 
                              + " from " + parent.getName());
                }
                boolean publicPathFromParentToChild = child.isPublic();
                if (DEBUG)
                {
                    Log.debug("Path from " + parent.getName() + " to " 
                              + child.getName() + " is " 
                              + (publicPathFromParentToChild
                                 ?"public":"private"));
                }
                addPath(parent, child, publicPathFromParentToChild);
                
                //We can call this since we just called the recursive
                //buildPathsFromParentToChildren.
                List publicRecursiveChildren = (List)publicMappings.get(child);
                if (publicRecursiveChildren == null)
                {
                    publicRecursiveChildren = new ArrayList(0);
                }
                if (DEBUG)
                {
                    Log.debug(child.getName() + " has " 
                              + publicRecursiveChildren.size() 
                              + " public recursive children.");
                }
                for (Iterator j = publicRecursiveChildren.iterator(); 
                     j.hasNext();)
                {
                    Project recursiveChild = (Project)j.next();
                    if (DEBUG)
                    {
                        Log.debug((publicPathFromParentToChild
                                   ? "Public" : "No-longer-public ")
                                  + " recursive path exists from " 
                                  + parent.getName() + " to child "
                                  + recursiveChild.getName());
                    }

                    //A public path from child to recursive child
                    //corresponds to a public path only if the path
                    //from parent to child is public
                    addPath(parent, recursiveChild, 
                            publicPathFromParentToChild);
                }

                List privateRecursiveChildren = 
                    (List)privateMappings.get(child);
                if (privateRecursiveChildren == null)
                {
                    privateRecursiveChildren = new ArrayList(0);
                }
                if (DEBUG)
                {
                    Log.debug(child.getName() + " has " 
                              + privateRecursiveChildren.size() 
                              + " private recursive children.");
                }
                for (Iterator j = privateRecursiveChildren.iterator(); 
                     j.hasNext();)
                {
                    Project recursiveChild = (Project)j.next();
                    if (DEBUG)
                    {                        
                        Log.debug("Private recursive path exists from " 
                                  + parent.getName() + " to child " 
                                  + recursiveChild.getName());
                    }
                    //A private path cannot become public.
                    addPath(parent, recursiveChild, false);
                }
            }
            if (DEBUG)
            {
                Log.debug("Done processing direct child " + child.getName());
            }
        }
        circularReferenceDetector.remove(parent);
        if (DEBUG)
        {
            Log.debug("Done with buildPathsFromParentToChildren, parent " 
                      + parent.getName());
        }
        alreadyProcessedProjects.add(parent);
    }

    /**
     * Adds the specified path to the list of mappings to insert into
     * the database. A public path supercedes a private one.
     */
    private void addPath (Project parent, Project child, boolean publicPath)
        throws Exception
    {
        Map mapping = (publicPath?publicMappings:privateMappings);

        //If the path is public, it supercedes any previously noted
        //private path between the two projects.
        if (publicPath)
        {
            List privateChildren = (List)privateMappings.get(parent);
            if (privateChildren != null)
            {
                privateChildren.remove(child);
                privateMappings.put(parent, privateChildren);
            }
        }

        List children = (List)mapping.get(parent);
        if (children == null)
        {
            children = new ArrayList(1);
        }
        children.add(child);
        mapping.put(parent,children);
    }

    public String describeContents()
    {
        return ("Contents described in database table.");
    }
}
@


1.6
log
@Cleanup of caching and cache invalidation.
@
text
@@


1.5
log
@Fixed bug which was causing denormalized table to look a lot like the undenormalized table.
@
text
@d110 1
a110 1
    private static String[] interestingDescriptors = 
d112 3
a114 3
        ProjectProjectgroup.class.getName(),
        Project.class.getName(),
        DenormalizedProjectProjectgroup.class.getName()
a116 1

d124 1
a124 1
        super(interestingDescriptors);
d356 4
@


1.4
log
@Bug fixes for clustering.
@
text
@d97 3
a99 2
     * This Map keeps track of the path mappings to insert, so that only one
     * INSERT statement need be executed to insert all the mapping at once.
d101 8
a108 1
    private Map mappingsToInsert = new HashMap();
a125 1
        Log.debug("Foo.");
d163 3
a165 2
        mappingsToInsert.clear();        
        
d171 2
a172 1
        DenormalizedProjectProjectgroupPeer.replaceMappings(mappingsToInsert);
d227 2
a228 1
                    Log.debug(parent.getName() + " is a category, not processing"
d244 1
a244 2
                Boolean publicPathFromParentToChild = 
                    new Boolean(child.isPublic());
d249 2
a250 1
                              + (publicPathFromParentToChild.booleanValue() ? "public":"private"));
d256 5
a260 2
                List publicRecursiveChildren = child
                    .getRecursiveChildren(Boolean.TRUE);
d263 3
a265 1
                    Log.debug(child.getName() + " has " + publicRecursiveChildren.size() + " public recursive children.");
d273 2
a274 2
                        Log.debug((publicPathFromParentToChild.booleanValue() ? 
                                   "Public" : "No-longer-public ")
d287 6
a292 2
                List privateRecursiveChildren = child
                    .getRecursiveChildren(Boolean.FALSE);
d310 1
a310 1
                    addPath(parent, recursiveChild, Boolean.FALSE);
d321 2
a322 1
            Log.debug("Done with buildPathsFromParentToChildren, parent " + parent.getName());
d329 1
a329 2
     * the database, if a public path between the two given projects
     * does not already exist.  
d331 1
a331 2
    private void addPath (Project parent, Project child, 
                                 Boolean publicPath)
d334 16
a349 6
        List thisPath = new ArrayList();
        thisPath.add(parent);
        thisPath.add(child);
        Boolean existingMapping = (Boolean)mappingsToInsert.get(thisPath);
        if (existingMapping == null || (existingMapping == Boolean.FALSE && 
                                        publicPath == Boolean.TRUE))
d351 1
a351 1
            mappingsToInsert.put(thisPath, publicPath);
d353 2
@


1.3
log
@Moved clustering features out of RMI and into the database.
@
text
@d73 1
a73 1
    public static final boolean DEBUG = false;
d118 1
@


1.2
log
@Refactored a lot of code to get rid of the good-looking but very slow IssueTrackingAccount{Descriptor,Map} classes. Also cleaned up the script-running interface quite a bit.
@
text
@d102 8
d117 1
a117 1
        super(ProjectGraphDenormalizer.class);
d127 1
a127 1
    public void invalidate(Object invalidationDescriptor)
d129 1
a129 1
        if (affectedByInvalidation(invalidationDescriptor))
d131 1
a131 9
            try
            {
                rebuildDenormalizedTable();
            }
            catch (Exception e)
            {
                throw new Error
                    ("Rebuild of denormalized table failed!");
            }                    
d133 1
a133 10
    }

    /**
     * This cache is invalidated whenever a ProjectProjectgroup is changed.
     */
    protected final boolean affectedByInvalidation 
        (Object invalidationDescriptor)
    {
        boolean affects = false;
        if (invalidationDescriptor == null)
d135 3
a137 14
            affects = true;
        }
        else if (invalidationDescriptor instanceof String)
        {
            String objectClassName = (String)invalidationDescriptor;
            if (objectClassName.equals(ProjectProjectgroup.class.getName())
                || objectClassName.equals(Project.class.getName())
                || objectClassName.equals
                (DenormalizedProjectProjectgroup.class.getName()))
            {
                affects = true;
            }
        }
        return affects;
@


1.1
log
@A Heartbreaking Patch of Staggering Genius: adds capacity (but no interface) for deeply nested projects, categories, and project groups. As a side effect, gets rid of the hacky special 'all public projects in domain' group. There are probably bugs in this, and some of those bugs are probably security holes, so watch out.
@
text
@d108 12
d121 37
a157 1
        super(ProjectGraphDenormalizer.class);
a343 48
    }

    /**
     * Invalidates some portion of the cache.
     *
     * @@param invalidationDescriptor Description of the invalidation that 
     *                               should occur, or <code>null</code> for
     *                               complete invalidation.
     */
    public void invalidate(Object invalidationDescriptor)
    {
        if (affectedByInvalidation(invalidationDescriptor))
        {
            try
            {
                rebuildDenormalizedTable();
            }
            catch (Exception e)
            {
                throw new Error
                    ("Rebuild of denormalized table failed!");
            }                    
        }
    }

    /**
     * This cache is invalidated whenever a ProjectProjectgroup is changed.
     */
    protected final boolean affectedByInvalidation 
        (Object invalidationDescriptor)
    {
        boolean affects = false;
        if (invalidationDescriptor == null)
        {
            affects = true;
        }
        else if (invalidationDescriptor instanceof String)
        {
            String objectClassName = (String)invalidationDescriptor;
            if (objectClassName.equals(ProjectProjectgroup.class.getName())
                || objectClassName.equals(Project.class.getName())
                || objectClassName.equals
                (DenormalizedProjectProjectgroup.class.getName()))
            {
                affects = true;
            }
        }
        return affects;
@

