Merge branch '2.3-gae' into 2.3-gae-tools
diff --git a/src/main/java/freemarker/core/DebugBreak.java b/src/main/java/freemarker/core/DebugBreak.java
index ead0654..1de26f5 100644
--- a/src/main/java/freemarker/core/DebugBreak.java
+++ b/src/main/java/freemarker/core/DebugBreak.java
@@ -53,8 +53,9 @@
 package freemarker.core;
 
 import java.io.IOException;
+import java.io.Serializable;
 
-import freemarker.debug.impl.DebuggerService;
+import freemarker.debug.DebuggerService;
 import freemarker.template.TemplateException;
 
 /**
@@ -62,16 +63,21 @@
  */
 public class DebugBreak extends TemplateElement
 {
-    public DebugBreak(TemplateElement nestedBlock)
+    private final DebuggerService debuggerService;    
+    private final Serializable data;
+    
+    public DebugBreak(TemplateElement nestedBlock, DebuggerService debuggerService, Serializable data)
     {
         this.nestedBlock = nestedBlock;
         nestedBlock.parent = this;
         copyLocationFrom(nestedBlock);
+        this.debuggerService = debuggerService;
+        this.data = data;   
     }
     
     protected void accept(Environment env) throws TemplateException, IOException
     {
-        if(!DebuggerService.suspendEnvironment(env, this.getTemplate().getName(), nestedBlock.getBeginLine()))
+        if(!debuggerService.suspendEnvironment(env, getTemplate().getName(), nestedBlock.getBeginLine(), data))
         {
             nestedBlock.accept(env);
         }
diff --git a/src/main/java/freemarker/debug/Breakpoint.java b/src/main/java/freemarker/debug/Breakpoint.java
index d54e910..d12c7cc 100644
--- a/src/main/java/freemarker/debug/Breakpoint.java
+++ b/src/main/java/freemarker/debug/Breakpoint.java
@@ -64,16 +64,28 @@
 
     private final String templateName;
     private final int line;
+    private final Serializable data;
+
+    public Breakpoint(String templateName, int line) {
+        this(templateName, line, null);
+    }
     
     /**
      * Creates a new breakpoint.
      * @param templateName the name of the template
      * @param line the line number in the template where to put the breakpoint
+     * @param data arbitrary data attached to this breakpoint, that the debugger client will get back with the
+     *        suspension event. As this data will travel back and forth between the client and server, <b>it has to be
+     *        de-serializable on the server</b>. Also, it shouldn't be very big serialized, as it will be possibly sent
+     *        through network. So if the data is big or might not be de-serializable on another JVM (missing classes), 
+     *        the client should just use an identifier as data (like a {@link Long}) with which later it can get
+     *        the actual data that's only stored on the client.
      */
-    public Breakpoint(String templateName, int line)
+    public Breakpoint(String templateName, int line, Serializable data)
     {
         this.templateName = templateName;
         this.line = line;
+        this.data = data;
     }
 
     /**
@@ -83,6 +95,7 @@
     {
         return line;
     }
+    
     /**
      * Returns the template name of the breakpoint
      */
@@ -91,6 +104,14 @@
         return templateName;
     }
 
+    /**
+     * Returns the arbitrary data attached to this breakpoint.
+     */
+    public Serializable getData()
+    {
+        return data;
+    }
+    
     public int hashCode()
     {
         return templateName.hashCode() + 31 * line;
diff --git a/src/main/java/freemarker/debug/DebuggerService.java b/src/main/java/freemarker/debug/DebuggerService.java
new file mode 100644
index 0000000..dd9a344
--- /dev/null
+++ b/src/main/java/freemarker/debug/DebuggerService.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2003 The Visigoth Software Society. 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 acknowledgement:
+ *       "This product includes software developed by the
+ *        Visigoth Software Society (http://www.visigoths.org/)."
+ *    Alternately, this acknowledgement may appear in the software itself,
+ *    if and wherever such third-party acknowledgements normally appear.
+ *
+ * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the 
+ *    project contributors may be used to endorse or promote products derived
+ *    from this software without prior written permission. For written
+ *    permission, please contact visigoths@visigoths.org.
+ *
+ * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
+ *    nor may "FreeMarker" or "Visigoth" appear in their names
+ *    without prior written permission of the Visigoth Software Society.
+ *
+ * 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 THE VISIGOTH SOFTWARE SOCIETY 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 the Visigoth Software Society. For more
+ * information on the Visigoth Software Society, please see
+ * http://www.visigoths.org/
+ */
+
+package freemarker.debug;
+
+import java.io.Serializable;
+import java.rmi.RemoteException;
+
+import freemarker.core.Environment;
+import freemarker.template.Template;
+
+/**
+ * This class provides debugging hooks for the core FreeMarker engine. It is not
+ * usable for anyone outside the FreeMarker core classes. It is public only as
+ * an implementation detail.
+ * 
+ * @author Attila Szegedi
+ * @version $Id: DebuggerService.java,v 1.2 2003/05/30 16:51:53 szegedia Exp $
+ */
+public interface DebuggerService {
+
+    void addBreakpoint(Breakpoint breakpoint);
+
+    void removeBreakpoint(Breakpoint breakpoint);
+
+    boolean suspendEnvironment(Environment env, String templateName, int line, Serializable data) throws
+    RemoteException;
+    
+    void registerTemplate(Template template);
+
+    void removeDebuggerListener(Object id);
+
+    Object addDebuggerListener(DebuggerListener listener);
+    
+    void shutdown();
+
+}
\ No newline at end of file
diff --git a/src/main/java/freemarker/debug/EnvironmentSuspendedEvent.java b/src/main/java/freemarker/debug/EnvironmentSuspendedEvent.java
index 360bb66..b9bc2cc 100644
--- a/src/main/java/freemarker/debug/EnvironmentSuspendedEvent.java
+++ b/src/main/java/freemarker/debug/EnvironmentSuspendedEvent.java
@@ -1,5 +1,6 @@
 package freemarker.debug;
 
+import java.io.Serializable;
 import java.util.EventObject;
 
 /**
@@ -13,13 +14,15 @@
 
     private final String name;
     private final int line;
+    private final Serializable data;
     private final DebuggedEnvironment env;
 
-    public EnvironmentSuspendedEvent(Object source, String templateName, int line, DebuggedEnvironment env)
+    public EnvironmentSuspendedEvent(Object source, String templateName, int line, Serializable data, DebuggedEnvironment env)
     {
         super(source);
         this.name = templateName;
         this.line = line;
+        this.data = data;
         this.env = env;
     }
 
@@ -42,6 +45,14 @@
     {
         return line;
     }
+    
+    /**
+     * Returns the arbitrary data that the debugger clients has attached to the {@link Breakpoint};
+     * possibly {@code null}.
+     */
+    public Serializable getData() {
+        return data;
+    }
 
     /**
      * The environment that was suspended
diff --git a/src/main/java/freemarker/debug/impl/AbstractDebuggerService.java b/src/main/java/freemarker/debug/impl/AbstractDebuggerService.java
new file mode 100644
index 0000000..0c55cb4
--- /dev/null
+++ b/src/main/java/freemarker/debug/impl/AbstractDebuggerService.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (c) 2003 The Visigoth Software Society. 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 acknowledgement:
+ *       "This product includes software developed by the
+ *        Visigoth Software Society (http://www.visigoths.org/)."
+ *    Alternately, this acknowledgement may appear in the software itself,
+ *    if and wherever such third-party acknowledgements normally appear.
+ *
+ * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the 
+ *    project contributors may be used to endorse or promote products derived
+ *    from this software without prior written permission. For written
+ *    permission, please contact visigoths@visigoths.org.
+ *
+ * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
+ *    nor may "FreeMarker" or "Visigoth" appear in their names
+ *    without prior written permission of the Visigoth Software Society.
+ *
+ * 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 THE VISIGOTH SOFTWARE SOCIETY 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 the Visigoth Software Society. For more
+ * information on the Visigoth Software Society, please see
+ * http://www.visigoths.org/
+ */
+
+package freemarker.debug.impl;
+
+import java.io.Serializable;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.rmi.RemoteException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.core.DebugBreak;
+import freemarker.core.Environment;
+import freemarker.core.TemplateElement;
+import freemarker.debug.Breakpoint;
+import freemarker.debug.DebuggerListener;
+import freemarker.debug.DebuggerService;
+import freemarker.debug.EnvironmentSuspendedEvent;
+import freemarker.template.Template;
+
+/**
+ * @author Attila Szegedi
+ * @version $Id
+ */
+public abstract class AbstractDebuggerService
+implements
+    DebuggerService
+{
+    private final Map templateDebugInfos = new HashMap();
+    private final HashSet suspendedEnvironments = new HashSet();
+    private final Map listeners = new HashMap();
+    private final ReferenceQueue refQueue = new ReferenceQueue();
+    
+    public List getBreakpoints(String templateName)
+    {
+        synchronized(templateDebugInfos)
+        {
+            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+            return tdi == null ? Collections.EMPTY_LIST : tdi.breakpoints;
+        }
+    }
+
+    public List getBreakpoints()
+    {
+        List sumlist = new ArrayList();
+        synchronized(templateDebugInfos)
+        {
+            for (Iterator iter = templateDebugInfos.values().iterator(); iter.hasNext();)
+            {
+                sumlist.addAll(((TemplateDebugInfo) iter.next()).breakpoints);
+            }
+        }
+        Collections.sort(sumlist);
+        return sumlist;
+    }
+
+    public boolean suspendEnvironment(Environment env, String templateName, int line, Serializable data)
+    throws
+        RemoteException
+    {
+        RmiDebuggedEnvironmentImpl denv = 
+            (RmiDebuggedEnvironmentImpl)
+                RmiDebuggedEnvironmentImpl.getCachedWrapperFor(env);
+                
+        synchronized(suspendedEnvironments)
+        {
+            suspendedEnvironments.add(denv);
+        }
+        try
+        {
+            EnvironmentSuspendedEvent breakpointEvent = 
+                new EnvironmentSuspendedEvent(this, templateName, line, data, denv);
+    
+            synchronized(listeners)
+            {
+                for (Iterator iter = listeners.values().iterator(); iter.hasNext();)
+                {
+                    DebuggerListener listener = (DebuggerListener) iter.next();
+                    listener.environmentSuspended(breakpointEvent);
+                }
+            }
+            synchronized(denv)
+            {
+                try
+                {
+                    denv.wait();
+                }
+                catch(InterruptedException e)
+                {
+                    ;// Intentionally ignored
+                }
+            }
+            return denv.isStopped();
+        }
+        finally
+        {
+            synchronized(suspendedEnvironments)
+            {
+                suspendedEnvironments.remove(denv);
+            }
+        }
+    }
+    
+    public void registerTemplate(Template template)
+    {
+        String templateName = template.getName();
+        synchronized(templateDebugInfos)
+        {
+            TemplateDebugInfo tdi = createTemplateDebugInfo(templateName);
+            tdi.templates.add(new TemplateReference(templateName, template, refQueue));
+            // Inject already defined breakpoints into the template
+            for (Iterator iter = tdi.breakpoints.iterator(); iter.hasNext();)
+            {
+                Breakpoint breakpoint = (Breakpoint) iter.next();
+                insertDebugBreak(template, breakpoint);
+            }
+        }
+    }
+    
+    Collection getSuspendedEnvironments()
+    {
+        return (Collection)suspendedEnvironments.clone();
+    }
+
+    public Object addDebuggerListener(DebuggerListener listener)
+    {
+        Object id; 
+        synchronized(listeners)
+        {
+            id = new Long(System.currentTimeMillis());
+            listeners.put(id, listener);
+        }
+        return id;
+    }
+    
+    public void removeDebuggerListener(Object id)
+    {
+        synchronized(listeners)
+        {
+            listeners.remove(id);
+        }
+    }
+
+    public void addBreakpoint(Breakpoint breakpoint)
+    {
+        String templateName = breakpoint.getTemplateName();
+        synchronized(templateDebugInfos)
+        {
+            TemplateDebugInfo tdi = createTemplateDebugInfo(templateName);
+            List breakpoints = tdi.breakpoints;
+            int pos = Collections.binarySearch(breakpoints, breakpoint);
+            if(pos < 0)
+            {
+                // Add to the list of breakpoints
+                breakpoints.add(-pos - 1, breakpoint);
+                // Inject the breakpoint into all templates with this name
+                for (Iterator iter = tdi.templates.iterator(); iter.hasNext();)
+                {
+                    TemplateReference ref = (TemplateReference) iter.next();
+                    Template t = ref.getTemplate();
+                    if(t == null)
+                    {
+                        iter.remove();
+                    }
+                    else
+                    {
+                        insertDebugBreak(t, breakpoint);
+                    }
+                }
+            }
+        }
+    }
+
+    private void insertDebugBreak(Template t, Breakpoint breakpoint)
+    {
+        TemplateElement te = findTemplateElement(t.getRootTreeNode(), breakpoint.getLine());
+        if(te == null)
+        {
+            return;
+        }
+        TemplateElement parent = (TemplateElement)te.getParent();
+        DebugBreak db = new DebugBreak(te, this, breakpoint.getData());   
+        // TODO: Ensure there always is a parent by making sure
+        // that the root element in the template is always a MixedContent
+        // Also make sure it doesn't conflict with anyone's code.
+        parent.setChildAt(parent.getIndex(te), db);
+    }
+
+    private static TemplateElement findTemplateElement(TemplateElement te, int line)
+    {
+        if(te.getBeginLine() > line || te.getEndLine() < line)
+        {
+            return null;
+        }
+        // Find the narrowest match
+        List childMatches = new ArrayList();
+        for(Enumeration children = te.children(); children.hasMoreElements();)
+        {
+            TemplateElement child = (TemplateElement)children.nextElement();
+            TemplateElement childmatch = findTemplateElement(child, line);
+            if(childmatch != null)
+            {
+                childMatches.add(childmatch);
+            }
+        }
+        //find a match that exactly matches the begin/end line
+        TemplateElement bestMatch = null;
+        for(int i = 0; i < childMatches.size(); i++)
+        {
+            TemplateElement e = (TemplateElement) childMatches.get(i);
+
+            if( bestMatch == null )
+            {
+                bestMatch = e;
+            }
+
+            if( e.getBeginLine() == line && e.getEndLine() > line )
+            {
+                bestMatch = e;
+            }
+
+            if( e.getBeginLine() == e.getEndLine() && e.getBeginLine() == line)
+            {
+                bestMatch = e;
+                break;
+            }
+        }
+        if( bestMatch != null)
+        {
+           return bestMatch;
+        }
+        // If no child provides narrower match, return this
+        return te;
+    }
+    
+    private TemplateDebugInfo findTemplateDebugInfo(String templateName)
+    {
+        processRefQueue();
+        return (TemplateDebugInfo)templateDebugInfos.get(templateName); 
+    }
+    
+    private TemplateDebugInfo createTemplateDebugInfo(String templateName)
+    {
+        TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+        if(tdi == null)
+        {
+            tdi = new TemplateDebugInfo();
+            templateDebugInfos.put(templateName, tdi);
+        }
+        return tdi;
+    }
+    
+    public void removeBreakpoint(Breakpoint breakpoint)
+    {
+        String templateName = breakpoint.getTemplateName();
+        synchronized(templateDebugInfos)
+        {
+            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+            if(tdi != null)
+            {
+                List breakpoints = tdi.breakpoints;
+                int pos = Collections.binarySearch(breakpoints, breakpoint);
+                if(pos >= 0)
+                { 
+                    breakpoints.remove(pos);
+                    for (Iterator iter = tdi.templates.iterator(); iter.hasNext();)
+                    {
+                        TemplateReference ref = (TemplateReference) iter.next();
+                        Template t = ref.getTemplate();
+                        if(t == null)
+                        {
+                            iter.remove();
+                        }
+                        else
+                        {
+                            removeDebugBreak(t, breakpoint);
+                        }
+                    }
+                }
+                if(tdi.isEmpty())
+                {
+                    templateDebugInfos.remove(templateName);
+                }
+            }
+        }
+    }
+
+    private void removeDebugBreak(Template t, Breakpoint breakpoint)
+    {
+        TemplateElement te = findTemplateElement(t.getRootTreeNode(), breakpoint.getLine());
+        if(te == null)
+        {
+            return;
+        }
+        DebugBreak db = null;
+        while(te != null)
+        {
+            if(te instanceof DebugBreak)
+            {
+                db = (DebugBreak)te;
+                break;
+            }
+            te = (TemplateElement)te.getParent();
+        }
+        if(db == null)
+        {
+            return;
+        }
+        TemplateElement parent = (TemplateElement)db.getParent(); 
+        parent.setChildAt(parent.getIndex(db), (TemplateElement)db.getChildAt(0));
+    }
+    
+    void removeBreakpoints(String templateName)
+    {
+        synchronized(templateDebugInfos)
+        {
+            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
+            if(tdi != null)
+            {
+                removeBreakpoints(tdi);
+                if(tdi.isEmpty())
+                {
+                    templateDebugInfos.remove(templateName);
+                }
+            }
+        }
+    }
+
+    void removeBreakpoints()
+    {
+        synchronized(templateDebugInfos)
+        {
+            for (Iterator iter = templateDebugInfos.values().iterator(); iter.hasNext();)
+            {
+                TemplateDebugInfo tdi = (TemplateDebugInfo) iter.next(); 
+                removeBreakpoints(tdi);
+                if(tdi.isEmpty())
+                {
+                    iter.remove();
+                }
+            }
+        }
+    }
+
+    private void removeBreakpoints(TemplateDebugInfo tdi)
+    {
+        tdi.breakpoints.clear();
+        for (Iterator iter = tdi.templates.iterator(); iter.hasNext();)
+        {
+            TemplateReference ref = (TemplateReference) iter.next();
+            Template t = ref.getTemplate();
+            if(t == null)
+            {
+                iter.remove();
+            }
+            else
+            {
+                removeDebugBreaks(t.getRootTreeNode());
+            }
+        }
+    }
+    
+    private void removeDebugBreaks(TemplateElement te)
+    {
+        int count = te.getChildCount();
+        for(int i = 0; i < count; ++i)
+        {
+            TemplateElement child = (TemplateElement)te.getChildAt(i);
+            while(child instanceof DebugBreak)
+            {
+                TemplateElement dbchild = (TemplateElement)child.getChildAt(0); 
+                te.setChildAt(i, dbchild);
+                child = dbchild;
+            }
+            removeDebugBreaks(child);
+        }
+    }
+    
+    private static final class TemplateDebugInfo
+    {
+        final List templates = new ArrayList();
+        final List breakpoints = new ArrayList();
+        
+        boolean isEmpty()
+        {
+            return templates.isEmpty() && breakpoints.isEmpty();
+        }
+    }
+    
+    private static final class TemplateReference extends WeakReference
+    {
+        final String templateName;
+         
+        TemplateReference(String templateName, Template template, ReferenceQueue queue)
+        {
+            super(template, queue);
+            this.templateName = templateName;
+        }
+        
+        Template getTemplate()
+        {
+            return (Template)get();
+        }
+    }
+    
+    private void processRefQueue()
+    {
+        for(;;)
+        {
+            TemplateReference ref = (TemplateReference)refQueue.poll();
+            if(ref == null)
+            {
+                break;
+            }
+            TemplateDebugInfo tdi = findTemplateDebugInfo(ref.templateName);
+            if(tdi != null)
+            {
+                tdi.templates.remove(ref);
+                if(tdi.isEmpty())
+                {
+                    templateDebugInfos.remove(ref.templateName);
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/freemarker/debug/impl/DebuggerService.java b/src/main/java/freemarker/debug/impl/DebuggerService.java
deleted file mode 100644
index c64762d..0000000
--- a/src/main/java/freemarker/debug/impl/DebuggerService.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (c) 2003 The Visigoth Software Society. 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 acknowledgement:
- *       "This product includes software developed by the
- *        Visigoth Software Society (http://www.visigoths.org/)."
- *    Alternately, this acknowledgement may appear in the software itself,
- *    if and wherever such third-party acknowledgements normally appear.
- *
- * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the 
- *    project contributors may be used to endorse or promote products derived
- *    from this software without prior written permission. For written
- *    permission, please contact visigoths@visigoths.org.
- *
- * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
- *    nor may "FreeMarker" or "Visigoth" appear in their names
- *    without prior written permission of the Visigoth Software Society.
- *
- * 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 THE VISIGOTH SOFTWARE SOCIETY 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 the Visigoth Software Society. For more
- * information on the Visigoth Software Society, please see
- * http://www.visigoths.org/
- */
-
-package freemarker.debug.impl;
-
-import java.rmi.RemoteException;
-import java.util.Collections;
-import java.util.List;
-
-import freemarker.core.Environment;
-import freemarker.template.Template;
-import freemarker.template.utility.SecurityUtilities;
-
-/**
- * This class provides debugging hooks for the core FreeMarker engine. It is
- * not usable for anyone outside the FreeMarker core classes. It is public only
- * as an implementation detail.
- * @author Attila Szegedi
- */
-public abstract class DebuggerService
-{
-    private static final DebuggerService instance = createInstance();
-    
-    private static DebuggerService createInstance()
-    {
-        // Creates the appropriate service class. If the debugging is turned
-        // off, this is a fast no-op service, otherwise it's the real-thing
-        // RMI service.
-        return 
-            SecurityUtilities.getSystemProperty("freemarker.debug.password") == null
-            ? (DebuggerService)new NoOpDebuggerService()
-            : (DebuggerService)new RmiDebuggerService();
-    }
-
-    public static List getBreakpoints(String templateName)
-    {
-        return instance.getBreakpointsSpi(templateName);
-    }
-    
-    abstract List getBreakpointsSpi(String templateName);
-
-    public static void registerTemplate(Template template)
-    {
-        instance.registerTemplateSpi(template);
-    }
-    
-    abstract void registerTemplateSpi(Template template);
-    
-    public static boolean suspendEnvironment(Environment env, String templateName, int line)
-    throws
-        RemoteException
-    {
-        return instance.suspendEnvironmentSpi(env, templateName, line);
-    }
-    
-    abstract boolean suspendEnvironmentSpi(Environment env, String templateName, int line)
-    throws
-        RemoteException;
-
-    abstract void shutdownSpi();
-
-    public static void shutdown()
-    {
-        instance.shutdownSpi();
-    }
-
-    private static class NoOpDebuggerService extends DebuggerService
-    {
-        List getBreakpointsSpi(String templateName)
-        {
-            return Collections.EMPTY_LIST;
-        }
-        
-        boolean suspendEnvironmentSpi(Environment env, String templateName, int line)
-        {
-            throw new UnsupportedOperationException();
-        }
-        
-        void registerTemplateSpi(Template template)
-        {
-        }
-
-        void shutdownSpi()
-        {
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/freemarker/debug/impl/RmiDebuggerImpl.java b/src/main/java/freemarker/debug/impl/RmiDebuggerImpl.java
index 438c0b4..0e46713 100644
--- a/src/main/java/freemarker/debug/impl/RmiDebuggerImpl.java
+++ b/src/main/java/freemarker/debug/impl/RmiDebuggerImpl.java
@@ -39,12 +39,12 @@
 
     public List getBreakpoints()
     {
-        return service.getBreakpointsSpi();
+        return service.getBreakpoints();
     }
 
     public List getBreakpoints(String templateName)
     {
-        return service.getBreakpointsSpi(templateName);
+        return service.getBreakpoints(templateName);
     }
 
     public Collection getSuspendedEnvironments()
diff --git a/src/main/java/freemarker/debug/impl/RmiDebuggerService.java b/src/main/java/freemarker/debug/impl/RmiDebuggerService.java
index 754becd..0fde850 100644
--- a/src/main/java/freemarker/debug/impl/RmiDebuggerService.java
+++ b/src/main/java/freemarker/debug/impl/RmiDebuggerService.java
@@ -53,28 +53,10 @@
 package freemarker.debug.impl;
 
 import java.io.Serializable;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
 import java.rmi.RemoteException;
 import java.rmi.server.RemoteObject;
 import java.rmi.server.UnicastRemoteObject;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
 
-import freemarker.core.DebugBreak;
-import freemarker.core.Environment;
-import freemarker.core.TemplateElement;
-import freemarker.debug.Breakpoint;
-import freemarker.debug.DebuggerListener;
-import freemarker.debug.EnvironmentSuspendedEvent;
-import freemarker.template.Template;
 import freemarker.template.utility.UndeclaredThrowableException;
 
 /**
@@ -83,14 +65,9 @@
  */
 class RmiDebuggerService
 extends
-    DebuggerService
+    AbstractDebuggerService
 {
-    private final Map templateDebugInfos = new HashMap();
-    private final HashSet suspendedEnvironments = new HashSet();
-    private final Map listeners = new HashMap();
-    private final ReferenceQueue refQueue = new ReferenceQueue();
-     
-
+  
     private final RmiDebuggerImpl debugger;
     private DebuggerServer server;
 
@@ -109,400 +86,7 @@
         }
     }
     
-    List getBreakpointsSpi(String templateName)
-    {
-        synchronized(templateDebugInfos)
-        {
-            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
-            return tdi == null ? Collections.EMPTY_LIST : tdi.breakpoints;
-        }
-    }
-
-    List getBreakpointsSpi()
-    {
-        List sumlist = new ArrayList();
-        synchronized(templateDebugInfos)
-        {
-            for (Iterator iter = templateDebugInfos.values().iterator(); iter.hasNext();)
-            {
-                sumlist.addAll(((TemplateDebugInfo) iter.next()).breakpoints);
-            }
-        }
-        Collections.sort(sumlist);
-        return sumlist;
-    }
-
-    boolean suspendEnvironmentSpi(Environment env, String templateName, int line)
-    throws
-        RemoteException
-    {
-        RmiDebuggedEnvironmentImpl denv = 
-            (RmiDebuggedEnvironmentImpl)
-                RmiDebuggedEnvironmentImpl.getCachedWrapperFor(env);
-                
-        synchronized(suspendedEnvironments)
-        {
-            suspendedEnvironments.add(denv);
-        }
-        try
-        {
-            EnvironmentSuspendedEvent breakpointEvent = 
-                new EnvironmentSuspendedEvent(this, templateName, line, denv);
-    
-            synchronized(listeners)
-            {
-                for (Iterator iter = listeners.values().iterator(); iter.hasNext();)
-                {
-                    DebuggerListener listener = (DebuggerListener) iter.next();
-                    listener.environmentSuspended(breakpointEvent);
-                }
-            }
-            synchronized(denv)
-            {
-                try
-                {
-                    denv.wait();
-                }
-                catch(InterruptedException e)
-                {
-                    ;// Intentionally ignored
-                }
-            }
-            return denv.isStopped();
-        }
-        finally
-        {
-            synchronized(suspendedEnvironments)
-            {
-                suspendedEnvironments.remove(denv);
-            }
-        }
-    }
-    
-    void registerTemplateSpi(Template template)
-    {
-        String templateName = template.getName();
-        synchronized(templateDebugInfos)
-        {
-            TemplateDebugInfo tdi = createTemplateDebugInfo(templateName);
-            tdi.templates.add(new TemplateReference(templateName, template, refQueue));
-            // Inject already defined breakpoints into the template
-            for (Iterator iter = tdi.breakpoints.iterator(); iter.hasNext();)
-            {
-                Breakpoint breakpoint = (Breakpoint) iter.next();
-                insertDebugBreak(template, breakpoint);
-            }
-        }
-    }
-    
-    Collection getSuspendedEnvironments()
-    {
-        return (Collection)suspendedEnvironments.clone();
-    }
-
-    Object addDebuggerListener(DebuggerListener listener)
-    {
-        Object id; 
-        synchronized(listeners)
-        {
-            id = new Long(System.currentTimeMillis());
-            listeners.put(id, listener);
-        }
-        return id;
-    }
-    
-    void removeDebuggerListener(Object id)
-    {
-        synchronized(listeners)
-        {
-            listeners.remove(id);
-        }
-    }
-
-    void addBreakpoint(Breakpoint breakpoint)
-    {
-        String templateName = breakpoint.getTemplateName();
-        synchronized(templateDebugInfos)
-        {
-            TemplateDebugInfo tdi = createTemplateDebugInfo(templateName);
-            List breakpoints = tdi.breakpoints;
-            int pos = Collections.binarySearch(breakpoints, breakpoint);
-            if(pos < 0)
-            {
-                // Add to the list of breakpoints
-                breakpoints.add(-pos - 1, breakpoint);
-                // Inject the breakpoint into all templates with this name
-                for (Iterator iter = tdi.templates.iterator(); iter.hasNext();)
-                {
-                    TemplateReference ref = (TemplateReference) iter.next();
-                    Template t = ref.getTemplate();
-                    if(t == null)
-                    {
-                        iter.remove();
-                    }
-                    else
-                    {
-                        insertDebugBreak(t, breakpoint);
-                    }
-                }
-            }
-        }
-    }
-
-    private static void insertDebugBreak(Template t, Breakpoint breakpoint)
-    {
-        TemplateElement te = findTemplateElement(t.getRootTreeNode(), breakpoint.getLine());
-        if(te == null)
-        {
-            return;
-        }
-        TemplateElement parent = (TemplateElement)te.getParent();
-        DebugBreak db = new DebugBreak(te);
-        // TODO: Ensure there always is a parent by making sure
-        // that the root element in the template is always a MixedContent
-        // Also make sure it doesn't conflict with anyone's code.
-        parent.setChildAt(parent.getIndex(te), db);
-    }
-
-    private static TemplateElement findTemplateElement(TemplateElement te, int line)
-    {
-        if(te.getBeginLine() > line || te.getEndLine() < line)
-        {
-            return null;
-        }
-        // Find the narrowest match
-        List childMatches = new ArrayList();
-        for(Enumeration children = te.children(); children.hasMoreElements();)
-        {
-            TemplateElement child = (TemplateElement)children.nextElement();
-            TemplateElement childmatch = findTemplateElement(child, line);
-            if(childmatch != null)
-            {
-                childMatches.add(childmatch);
-            }
-        }
-        //find a match that exactly matches the begin/end line
-        TemplateElement bestMatch = null;
-        for(int i = 0; i < childMatches.size(); i++)
-        {
-            TemplateElement e = (TemplateElement) childMatches.get(i);
-
-            if( bestMatch == null )
-            {
-                bestMatch = e;
-            }
-
-            if( e.getBeginLine() == line && e.getEndLine() > line )
-            {
-                bestMatch = e;
-            }
-
-            if( e.getBeginLine() == e.getEndLine() && e.getBeginLine() == line)
-            {
-                bestMatch = e;
-                break;
-            }
-        }
-        if( bestMatch != null)
-        {
-           return bestMatch;
-        }
-        // If no child provides narrower match, return this
-        return te;
-    }
-    
-    private TemplateDebugInfo findTemplateDebugInfo(String templateName)
-    {
-        processRefQueue();
-        return (TemplateDebugInfo)templateDebugInfos.get(templateName); 
-    }
-    
-    private TemplateDebugInfo createTemplateDebugInfo(String templateName)
-    {
-        TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
-        if(tdi == null)
-        {
-            tdi = new TemplateDebugInfo();
-            templateDebugInfos.put(templateName, tdi);
-        }
-        return tdi;
-    }
-    
-    void removeBreakpoint(Breakpoint breakpoint)
-    {
-        String templateName = breakpoint.getTemplateName();
-        synchronized(templateDebugInfos)
-        {
-            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
-            if(tdi != null)
-            {
-                List breakpoints = tdi.breakpoints;
-                int pos = Collections.binarySearch(breakpoints, breakpoint);
-                if(pos >= 0)
-                { 
-                    breakpoints.remove(pos);
-                    for (Iterator iter = tdi.templates.iterator(); iter.hasNext();)
-                    {
-                        TemplateReference ref = (TemplateReference) iter.next();
-                        Template t = ref.getTemplate();
-                        if(t == null)
-                        {
-                            iter.remove();
-                        }
-                        else
-                        {
-                            removeDebugBreak(t, breakpoint);
-                        }
-                    }
-                }
-                if(tdi.isEmpty())
-                {
-                    templateDebugInfos.remove(templateName);
-                }
-            }
-        }
-    }
-
-    private void removeDebugBreak(Template t, Breakpoint breakpoint)
-    {
-        TemplateElement te = findTemplateElement(t.getRootTreeNode(), breakpoint.getLine());
-        if(te == null)
-        {
-            return;
-        }
-        DebugBreak db = null;
-        while(te != null)
-        {
-            if(te instanceof DebugBreak)
-            {
-                db = (DebugBreak)te;
-                break;
-            }
-            te = (TemplateElement)te.getParent();
-        }
-        if(db == null)
-        {
-            return;
-        }
-        TemplateElement parent = (TemplateElement)db.getParent(); 
-        parent.setChildAt(parent.getIndex(db), (TemplateElement)db.getChildAt(0));
-    }
-    
-    void removeBreakpoints(String templateName)
-    {
-        synchronized(templateDebugInfos)
-        {
-            TemplateDebugInfo tdi = findTemplateDebugInfo(templateName);
-            if(tdi != null)
-            {
-                removeBreakpoints(tdi);
-                if(tdi.isEmpty())
-                {
-                    templateDebugInfos.remove(templateName);
-                }
-            }
-        }
-    }
-
-    void removeBreakpoints()
-    {
-        synchronized(templateDebugInfos)
-        {
-            for (Iterator iter = templateDebugInfos.values().iterator(); iter.hasNext();)
-            {
-                TemplateDebugInfo tdi = (TemplateDebugInfo) iter.next(); 
-                removeBreakpoints(tdi);
-                if(tdi.isEmpty())
-                {
-                    iter.remove();
-                }
-            }
-        }
-    }
-
-    private void removeBreakpoints(TemplateDebugInfo tdi)
-    {
-        tdi.breakpoints.clear();
-        for (Iterator iter = tdi.templates.iterator(); iter.hasNext();)
-        {
-            TemplateReference ref = (TemplateReference) iter.next();
-            Template t = ref.getTemplate();
-            if(t == null)
-            {
-                iter.remove();
-            }
-            else
-            {
-                removeDebugBreaks(t.getRootTreeNode());
-            }
-        }
-    }
-    
-    private void removeDebugBreaks(TemplateElement te)
-    {
-        int count = te.getChildCount();
-        for(int i = 0; i < count; ++i)
-        {
-            TemplateElement child = (TemplateElement)te.getChildAt(i);
-            while(child instanceof DebugBreak)
-            {
-                TemplateElement dbchild = (TemplateElement)child.getChildAt(0); 
-                te.setChildAt(i, dbchild);
-                child = dbchild;
-            }
-            removeDebugBreaks(child);
-        }
-    }
-    
-    private static final class TemplateDebugInfo
-    {
-        final List templates = new ArrayList();
-        final List breakpoints = new ArrayList();
-        
-        boolean isEmpty()
-        {
-            return templates.isEmpty() && breakpoints.isEmpty();
-        }
-    }
-    
-    private static final class TemplateReference extends WeakReference
-    {
-        final String templateName;
-         
-        TemplateReference(String templateName, Template template, ReferenceQueue queue)
-        {
-            super(template, queue);
-            this.templateName = templateName;
-        }
-        
-        Template getTemplate()
-        {
-            return (Template)get();
-        }
-    }
-    
-    private void processRefQueue()
-    {
-        for(;;)
-        {
-            TemplateReference ref = (TemplateReference)refQueue.poll();
-            if(ref == null)
-            {
-                break;
-            }
-            TemplateDebugInfo tdi = findTemplateDebugInfo(ref.templateName);
-            if(tdi != null)
-            {
-                tdi.templates.remove(ref);
-                if(tdi.isEmpty())
-                {
-                    templateDebugInfos.remove(ref.templateName);
-                }
-            }
-        }
-    }
-
-    void shutdownSpi()
+    public void shutdown()
     {
         server.stop();
         try
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 2004e21..bc57b4e 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -88,6 +88,7 @@
 import freemarker.core._CoreAPI;
 import freemarker.core._DelayedJQuote;
 import freemarker.core._MiscTemplateException;
+import freemarker.debug.DebuggerService;
 import freemarker.template.utility.CaptureOutput;
 import freemarker.template.utility.ClassUtil;
 import freemarker.template.utility.HtmlEscape;
@@ -192,6 +193,8 @@
     private ArrayList autoImports = new ArrayList(), autoIncludes = new ArrayList(); 
     private Map autoImportNsToTmpMap = new HashMap();   // TODO No need for this, instead use List<NamespaceToTemplate> below.
 
+    private DebuggerService debuggerService;
+    
     public Configuration() {
         cache = new TemplateCache();
         cache.setConfiguration(this);
@@ -1247,4 +1250,12 @@
 		}
 		return s;
 	}
+	
+	public DebuggerService getDebuggerService() {
+        return debuggerService;
+    }
+	
+	public void setDebuggerService(DebuggerService debuggerService) {
+        this.debuggerService = debuggerService;
+    }
 }
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index 6589294..a64dedb 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -78,7 +78,6 @@
 import freemarker.core.TemplateElement;
 import freemarker.core.TextBlock;
 import freemarker.core.TokenMgrError;
-import freemarker.debug.impl.DebuggerService;
 
 /**
  * <p>Stores an already parsed template, ready to be processed (rendered) for unlimited times, possibly from
@@ -207,10 +206,17 @@
         finally {
             reader.close();
         }
-        DebuggerService.registerTemplate(this);
+        registerTemplateToDebugger(this, cfg);
         namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
         prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
     }
+    
+    static void registerTemplateToDebugger(Template template, Configuration cfg) {
+        freemarker.debug.DebuggerService debuggerService = cfg.getDebuggerService();
+        if (debuggerService != null) {
+            debuggerService.registerTemplate(template);
+        }
+    }
 
     /**
      * Equivalent to {@link #Template(String, Reader, Configuration)
@@ -230,7 +236,7 @@
     Template(String name, TemplateElement root, Configuration config) {
         this(name, config);
         this.rootElement = root;
-        DebuggerService.registerTemplate(this);
+        registerTemplateToDebugger(this, config);
     }
 
     /**
@@ -246,7 +252,7 @@
         Template template = new Template(name, config);
         TextBlock block = new TextBlock(content);
         template.rootElement = block;
-        DebuggerService.registerTemplate(template);
+        registerTemplateToDebugger(template, config);
         return template;
     }