[jbosstools-dev] Fwd: [jbosstools-commits] JBoss Tools SVN: r5415 - in trunk/core/plugins/org.jboss.ide.eclipse.archives.core: src/eclipse/org/jboss/ide/eclipse/archives/core and 5 other directories.
Max Rydahl Andersen
max.andersen at redhat.com
Mon Jan 14 17:32:06 EST 2008
On Fri, 11 Jan 2008 20:03:50 +0100, Rob Stryker <rob.stryker at redhat.com> wrote:
> Hi Max:
>
> Sorry for the delay. Short answer: being able to run outside of eclipse
> without bundling half of eclipse ;)
ok.
> Basically, I *could* use the eclipse version of the class and pass in
> my own IStringVariableManager, which would be responsible for getting
> the values of the variables. However, this class is stored in
> eclipse/plugins/org.eclipse.core.variables_3.2.0.v20070426.jar, which
> would be another jar that would need to be on the classpath.
> So basically, I stole their ${ } parsing logic and get the value on my own.
but why this System.getProperties() and .dir ending scanning - looks weird ?
> Because archives.core (the plugin) is made up of two jars,
> archivescore.jar and archivescore-eclipse.jar, the plugin itself will
> still depend on eclipse.core.variables... but the resultant
> archivescore.jar will not.
mkay.
>
> Hope this helps you understand what's going on =]
ok.
-max
> Again, sorry for the delay.
>
> - Rob
>
> Max Rydahl Andersen wrote:
>> Rob - the question about why StringSubstitution is cloned is still relevant.
>>
>> Why are we cloning public API ?
>>
>> -max
>>
>>
>>> Rob - why are we doing big feature changes in 2.0.1 code stream ?
>>>
>>> ...and why clone public API like StringSubstitution ?
>>>
>>> -max
>>>
>>> ------- Forwarded message -------
>>> From: jbosstools-commits at lists.jboss.org
>>> To: jbosstools-commits at lists.jboss.org
>>> Cc:
>>> Subject: [jbosstools-commits] JBoss Tools SVN: r5415 - in trunk/core/plugins/org.jboss.ide.eclipse.archives.core: src/eclipse/org/jboss/ide/eclipse/archives/core and 5 other directories.
>>> Date: Thu, 20 Dec 2007 21:21:48 +0100
>>>
>>> Author: rob.stryker at jboss.com
>>> Date: 2007-12-20 15:21:47 -0500 (Thu, 20 Dec 2007)
>>> New Revision: 5415
>>>
>>> Added:
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/xpl/
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/xpl/StringSubstitutionEngineClone.java
>>> Modified:
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/META-INF/MANIFEST.MF
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/ArchivesCorePlugin.java
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/model/other/internal/WorkspaceVariables.java
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/ArchivesCore.java
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/IRuntimeVariables.java
>>> trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/internal/StandaloneVariables.java
>>> Log:
>>> JBIDE-1406 prelim preparation
>>>
>>> Modified: trunk/core/plugins/org.jboss.ide.eclipse.archives.core/META-INF/MANIFEST.MF
>>> ===================================================================
>>> --- trunk/core/plugins/org.jboss.ide.eclipse.archives.core/META-INF/MANIFEST.MF 2007-12-20 19:20:25 UTC (rev 5414)
>>> +++ trunk/core/plugins/org.jboss.ide.eclipse.archives.core/META-INF/MANIFEST.MF 2007-12-20 20:21:47 UTC (rev 5415)
>>> @@ -11,7 +11,8 @@
>>> org.eclipse.core.commands,
>>> org.apache.ant,
>>> org.eclipse.core.resources,
>>> - org.eclipse.jdt.core
>>> + org.eclipse.jdt.core,
>>> + org.eclipse.core.variables
>>> Eclipse-LazyStart: true
>>> Bundle-ClassPath: lib/concurrent.jar,
>>> lib/truezip-6.jar,
>>>
>>> Modified: trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/ArchivesCorePlugin.java
>>> ===================================================================
>>> --- trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/ArchivesCorePlugin.java 2007-12-20 19:20:25 UTC (rev 5414)
>>> +++ trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/ArchivesCorePlugin.java 2007-12-20 20:21:47 UTC (rev 5415)
>>> @@ -34,7 +34,7 @@
>>> public class ArchivesCorePlugin extends Plugin {
>>>
>>> // The plug-in ID
>>> - public static final String PLUGIN_ID = "org.jboss.ide.eclipse.archives.core";
>>> + public static final String PLUGIN_ID = ArchivesCore.PLUGIN_ID;
>>>
>>> // The shared instance
>>> private static ArchivesCorePlugin plugin;
>>>
>>> Modified: trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/model/other/internal/WorkspaceVariables.java
>>> ===================================================================
>>> --- trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/model/other/internal/WorkspaceVariables.java 2007-12-20 19:20:25 UTC (rev 5414)
>>> +++ trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/eclipse/org/jboss/ide/eclipse/archives/core/model/other/internal/WorkspaceVariables.java 2007-12-20 20:21:47 UTC (rev 5415)
>>> @@ -4,8 +4,10 @@
>>>
>>> import org.eclipse.core.resources.IProject;
>>> import org.eclipse.core.resources.ResourcesPlugin;
>>> +import org.eclipse.core.runtime.CoreException;
>>> import org.eclipse.core.runtime.IPath;
>>> import org.eclipse.core.runtime.Platform;
>>> +import org.eclipse.core.variables.VariablesPlugin;
>>> import org.jboss.ide.eclipse.archives.core.ArchivesCorePlugin;
>>> import org.jboss.ide.eclipse.archives.core.model.other.IRuntimeVariables;
>>>
>>> @@ -37,4 +39,17 @@
>>> return ArchivesCorePlugin.getDefault().isDebugging()
>>> && "true".equalsIgnoreCase(Platform.getDebugOption(option));
>>> }
>>> +
>>> + public String getProjectName(IPath path) {
>>> + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
>>> + for( int i = 0; i < projects.length; i++ )
>>> + if( projects[i].getLocation().equals(path))
>>> + return projects[i].getName();
>>> + return null;
>>> + }
>>> +
>>> + public String performStringSubstitution(String expression,
>>> + boolean reportUndefinedVariables) throws CoreException {
>>> + return VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(expression, reportUndefinedVariables);
>>> + }
>>> }
>>>
>>> Modified: trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/ArchivesCore.java
>>> ===================================================================
>>> --- trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/ArchivesCore.java 2007-12-20 19:20:25 UTC (rev 5414)
>>> +++ trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/ArchivesCore.java 2007-12-20 20:21:47 UTC (rev 5415)
>>> @@ -7,6 +7,7 @@
>>>
>>> public abstract class ArchivesCore {
>>>
>>> + public static final String PLUGIN_ID = "org.jboss.ide.eclipse.archives.core";
>>> private static ArchivesCore instance;
>>> // Due to classloader restrictions we won't be able to lazy load, but that should be ok as long
>>> // as we keep the construction of ArchivesCore subclasses to a minimum
>>>
>>> Modified: trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/IRuntimeVariables.java
>>> ===================================================================
>>> --- trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/IRuntimeVariables.java 2007-12-20 19:20:25 UTC (rev 5414)
>>> +++ trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/IRuntimeVariables.java 2007-12-20 20:21:47 UTC (rev 5415)
>>> @@ -2,6 +2,7 @@
>>>
>>> import java.net.URL;
>>>
>>> +import org.eclipse.core.runtime.CoreException;
>>> import org.eclipse.core.runtime.IPath;
>>>
>>> public interface IRuntimeVariables {
>>> @@ -13,8 +14,11 @@
>>> */
>>> public boolean isDebugging(String option);
>>> public IPath getProjectPath(String projectName);
>>> -
>>> -// public IPath getWorkspacePath();
>>> + public String getProjectName(IPath path);
>>> public URL getBindingSchema();
>>> public URL getBindingLog4j();
>>> +
>>> + // allow for variable replacement
>>> + public String performStringSubstitution(String expression, boolean reportUndefinedVariables) throws CoreException;
>>> +
>>> }
>>>
>>> Modified: trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/internal/StandaloneVariables.java
>>> ===================================================================
>>> --- trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/internal/StandaloneVariables.java 2007-12-20 19:20:25 UTC (rev 5414)
>>> +++ trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/model/other/internal/StandaloneVariables.java 2007-12-20 20:21:47 UTC (rev 5415)
>>> @@ -1,10 +1,14 @@
>>> package org.jboss.ide.eclipse.archives.core.model.other.internal;
>>>
>>> import java.net.URL;
>>> +import java.util.Iterator;
>>> +import java.util.Properties;
>>>
>>> +import org.eclipse.core.runtime.CoreException;
>>> import org.eclipse.core.runtime.IPath;
>>> import org.eclipse.core.runtime.Path;
>>> import org.jboss.ide.eclipse.archives.core.model.other.IRuntimeVariables;
>>> +import org.jboss.ide.eclipse.archives.core.xpl.StringSubstitutionEngineClone;
>>>
>>> public class StandaloneVariables implements IRuntimeVariables {
>>>
>>> @@ -27,8 +31,27 @@
>>> }
>>>
>>> public boolean isDebugging(String option) {
>>> - // TODO Auto-generated method stub
>>> - return false;
>>> + return System.getProperty("archives.debug", "true")
>>> + .equals("true");
>>> }
>>>
>>> + public String getProjectName(IPath path) {
>>> + Properties props = System.getProperties();
>>> + Object key, val;
>>> + for( Iterator i = props.keySet().iterator(); i.hasNext(); ) {
>>> + key = i.next();
>>> + if( key instanceof String && ((String)key).endsWith(".dir")) {
>>> + val = props.get(key);
>>> + if( path.toOSString().equals(new Path((String)val).toOSString()))
>>> + return (String)key;
>>> + }
>>> + }
>>> + return null;
>>> + }
>>> +
>>> + public String performStringSubstitution(String expression,
>>> + boolean reportUndefinedVariables) throws CoreException {
>>> + return new StringSubstitutionEngineClone().performStringSubstitution(expression, reportUndefinedVariables);
>>> + }
>>> +
>>> }
>>>
>>> Added: trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/xpl/StringSubstitutionEngineClone.java
>>> ===================================================================
>>> --- trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/xpl/StringSubstitutionEngineClone.java (rev 0)
>>> +++ trunk/core/plugins/org.jboss.ide.eclipse.archives.core/src/main/org/jboss/ide/eclipse/archives/core/xpl/StringSubstitutionEngineClone.java 2007-12-20 20:21:47 UTC (rev 5415)
>>> @@ -0,0 +1,289 @@
>>> +/*******************************************************************************
>>> + * Copyright (c) 2000, 2006 IBM Corporation and others.
>>> + * All rights reserved. This program and the accompanying materials
>>> + * are made available under the terms of the Eclipse Public License v1.0
>>> + * which accompanies this distribution, and is available at
>>> + * http://www.eclipse.org/legal/epl-v10.html
>>> + *
>>> + * Contributors:
>>> + * IBM Corporation - initial API and implementation
>>> + *******************************************************************************/
>>> +package org.jboss.ide.eclipse.archives.core.xpl;
>>> +
>>> +import java.util.ArrayList;
>>> +import java.util.HashSet;
>>> +import java.util.Iterator;
>>> +import java.util.List;
>>> +import java.util.Stack;
>>> +import org.eclipse.core.runtime.CoreException;
>>> +import org.eclipse.core.runtime.IStatus;
>>> +import org.eclipse.core.runtime.Status;
>>> +import org.jboss.ide.eclipse.archives.core.ArchivesCore;
>>> +
>>> +/**
>>> + * Performs string substitution for context and value variables.
>>> + */
>>> +public class StringSubstitutionEngineClone {
>>> +
>>> + // delimiters
>>> + private static final String VARIABLE_START = "${"; //$NON-NLS-1$
>>> + private static final char VARIABLE_END = '}';
>>> + private static final char VARIABLE_ARG = ':';
>>> + // parsing states
>>> + private static final int SCAN_FOR_START = 0;
>>> + private static final int SCAN_FOR_END = 1;
>>> +
>>> + /**
>>> + * Resulting string
>>> + */
>>> + private StringBuffer fResult;
>>> +
>>> + /**
>>> + * Whether substitutions were performed
>>> + */
>>> + private boolean fSubs;
>>> +
>>> + /**
>>> + * Stack of variables to resolve
>>> + */
>>> + private Stack fStack;
>>> +
>>> + class VariableReference {
>>> +
>>> + // the text inside the variable reference
>>> + private StringBuffer fText;
>>> +
>>> + public VariableReference() {
>>> + fText = new StringBuffer();
>>> + }
>>> +
>>> + public void append(String text) {
>>> + fText.append(text);
>>> + }
>>> +
>>> + public String getText() {
>>> + return fText.toString();
>>> + }
>>> +
>>> + }
>>> +
>>> + /**
>>> + * Performs recursive string substitution and returns the resulting string.
>>> + *
>>> + * @param expression expression to resolve
>>> + * @param reportUndefinedVariables whether to report undefined variables as an error
>>> + * @return the resulting string with all variables recursively
>>> + * substituted
>>> + * @exception CoreException if unable to resolve a referenced variable or if a cycle exists
>>> + * in referenced variables
>>> + */
>>> + public String performStringSubstitution(String expression, boolean reportUndefinedVariables ) throws CoreException {
>>> + substitute(expression, reportUndefinedVariables );
>>> + List resolvedVariableSets = new ArrayList();
>>> + while (fSubs) {
>>> + HashSet resolved = substitute(fResult.toString(), reportUndefinedVariables );
>>> +
>>> + for(int i=resolvedVariableSets.size()-1; i>=0; i--) {
>>> +
>>> + HashSet prevSet = (HashSet)resolvedVariableSets.get(i);
>>> +
>>> + if (prevSet.equals(resolved)) {
>>> + HashSet conflictingSet = new HashSet();
>>> + for (; i<resolvedVariableSets.size(); i++)
>>> + conflictingSet.addAll((HashSet)resolvedVariableSets.get(i));
>>> +
>>> + StringBuffer problemVariableList = new StringBuffer();
>>> + for (Iterator it=conflictingSet.iterator(); it.hasNext(); ) {
>>> + problemVariableList.append(it.next().toString());
>>> + problemVariableList.append(", "); //$NON-NLS-1$
>>> + }
>>> + problemVariableList.setLength(problemVariableList.length()-2); //truncate the last ", "
>>> + Status status = new Status(IStatus.ERROR,
>>> + ArchivesCore.PLUGIN_ID,
>>> + "Cycle found in variable replacement",
>>> + null);
>>> + throw new CoreException(status);
>>> + }
>>> + }
>>> +
>>> + resolvedVariableSets.add(resolved);
>>> + }
>>> + return fResult.toString();
>>> + }
>>> +
>>> + /**
>>> + * Performs recursive string validation to ensure that all of the variables
>>> + * contained in the expression exist
>>> + * @param expression expression to validate
>>> + * @param manager registry of variables
>>> + * @exception CoreException if a referenced variable does not exist or if a cycle exists
>>> + * in referenced variables
>>> + */
>>> + public void validateStringVariables(String expression ) throws CoreException {
>>> + performStringSubstitution(expression, true );
>>> + }
>>> +
>>> + /**
>>> + * Makes a substitution pass of the given expression returns a Set of the variables that were resolved in this
>>> + * pass
>>> + *
>>> + * @param expression source expression
>>> + * @param reportUndefinedVariables whether to report undefined variables as an error
>>> + * @param resolveVariables whether to resolve the value of any variables
>>> + * @exception CoreException if unable to resolve a variable
>>> + */
>>> + private HashSet substitute(String expression, boolean reportUndefinedVariables) throws CoreException {
>>> + fResult = new StringBuffer(expression.length());
>>> + fStack = new Stack();
>>> + fSubs = false;
>>> +
>>> + HashSet resolvedVariables = new HashSet();
>>> +
>>> + int pos = 0;
>>> + int state = SCAN_FOR_START;
>>> + while (pos < expression.length()) {
>>> + switch (state) {
>>> + case SCAN_FOR_START:
>>> + int start = expression.indexOf(VARIABLE_START, pos);
>>> + if (start >= 0) {
>>> + int length = start - pos;
>>> + // copy non-variable text to the result
>>> + if (length > 0) {
>>> + fResult.append(expression.substring(pos, start));
>>> + }
>>> + pos = start + 2;
>>> + state = SCAN_FOR_END;
>>> +
>>> + fStack.push(new VariableReference());
>>> + } else {
>>> + // done - no more variables
>>> + fResult.append(expression.substring(pos));
>>> + pos = expression.length();
>>> + }
>>> + break;
>>> + case SCAN_FOR_END:
>>> + // be careful of nested variables
>>> + start = expression.indexOf(VARIABLE_START, pos);
>>> + int end = expression.indexOf(VARIABLE_END, pos);
>>> + if (end < 0) {
>>> + // variables are not completed
>>> + VariableReference tos = (VariableReference)fStack.peek();
>>> + tos.append(expression.substring(pos));
>>> + pos = expression.length();
>>> + } else {
>>> + if (start >= 0 && start < end) {
>>> + // start of a nested variable
>>> + int length = start - pos;
>>> + if (length > 0) {
>>> + VariableReference tos = (VariableReference)fStack.peek();
>>> + tos.append(expression.substring(pos, start));
>>> + }
>>> + pos = start + 2;
>>> + fStack.push(new VariableReference());
>>> + } else {
>>> + // end of variable reference
>>> + VariableReference tos = (VariableReference)fStack.pop();
>>> + String substring = expression.substring(pos, end);
>>> + tos.append(substring);
>>> + resolvedVariables.add(substring);
>>> +
>>> + pos = end + 1;
>>> + String value= resolve(tos, reportUndefinedVariables);
>>> + if (value == null) {
>>> + value = ""; //$NON-NLS-1$
>>> + }
>>> + if (fStack.isEmpty()) {
>>> + // append to result
>>> + fResult.append(value);
>>> + state = SCAN_FOR_START;
>>> + } else {
>>> + // append to previous variable
>>> + tos = (VariableReference)fStack.peek();
>>> + tos.append(value);
>>> + }
>>> + }
>>> + }
>>> + break;
>>> + }
>>> + }
>>> + // process incomplete variable references
>>> + while (!fStack.isEmpty()) {
>>> + VariableReference tos = (VariableReference)fStack.pop();
>>> + if (fStack.isEmpty()) {
>>> + fResult.append(VARIABLE_START);
>>> + fResult.append(tos.getText());
>>> + } else {
>>> + VariableReference var = (VariableReference)fStack.peek();
>>> + var.append(VARIABLE_START);
>>> + var.append(tos.getText());
>>> + }
>>> + }
>>> +
>>> +
>>> + return resolvedVariables;
>>> + }
>>> +
>>> + /**
>>> + * Resolve and return the value of the given variable reference,
>>> + * possibly <code>null</code>.
>>> + *
>>> + * @param var
>>> + * @param reportUndefinedVariables whether to report undefined variables as
>>> + * an error
>>> + * @return variable value, possibly <code>null</code>
>>> + * @exception CoreException if unable to resolve a value
>>> + */
>>> + private String resolve(VariableReference var, boolean reportUndefinedVariables) throws CoreException {
>>> +// String text = var.getText();
>>> +// int pos = text.indexOf(VARIABLE_ARG);
>>> +// String name = null;
>>> +// String arg = null;
>>> +// if (pos > 0) {
>>> +// name = text.substring(0, pos);
>>> +// pos++;
>>> +// if (pos < text.length()) {
>>> +// arg = text.substring(pos);
>>> +// }
>>> +// } else {
>>> +// name = text;
>>> +// }
>>> +// IValueVariable valueVariable = manager.getValueVariable(name);
>>> +// if (valueVariable == null) {
>>> +// IDynamicVariable dynamicVariable = manager.getDynamicVariable(name);
>>> +// if (dynamicVariable == null) {
>>> +// // no variables with the given name
>>> +// if (reportUndefinedVariables) {
>>> +// throw new CoreException(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), VariablesPlugin.INTERNAL_ERROR, NLS.bind(VariablesMessages.StringSubstitutionEngine_3, new String[]{name}), null));
>>> +// }
>>> +// // leave as is
>>> +// return getOriginalVarText(var);
>>> +// }
>>> +//
>>> +// if (resolveVariables) {
>>> +// fSubs = true;
>>> +// return dynamicVariable.getValue(arg);
>>> +// }
>>> +// //leave as is
>>> +// return getOriginalVarText(var);
>>> +// }
>>> +//
>>> +// if (arg == null) {
>>> +// if (resolveVariables) {
>>> +// fSubs = true;
>>> +// return valueVariable.getValue();
>>> +// }
>>> +// //leave as is
>>> +// return getOriginalVarText(var);
>>> +// }
>>> +// // error - an argument specified for a value variable
>>> +// throw new CoreException(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), VariablesPlugin.INTERNAL_ERROR, NLS.bind(VariablesMessages.StringSubstitutionEngine_4, new String[]{valueVariable.getName()}), null));
>>> + return "";
>>> + }
>>> +
>>> + private String getOriginalVarText(VariableReference var) {
>>> + StringBuffer res = new StringBuffer(var.getText());
>>> + res.insert(0, VARIABLE_START);
>>> + res.append(VARIABLE_END);
>>> + return res.toString();
>>> + }
>>> +}
>>>
>>> _______________________________________________
>>> jbosstools-commits mailing list
>>> jbosstools-commits at lists.jboss.org
>>> https://lists.jboss.org/mailman/listinfo/jbosstools-commits
>>>
>>>
>>>
>>>
>>
>>
>>
>>
>
>
--
Using Opera's revolutionary e-mail client: http://www.opera.com/mail/
More information about the jbosstools-dev
mailing list