Author: dabbous
Date: 2006-06-05 11:36:19-0700
New Revision: 10144
Added:
trunk/src/java/org/tigris/scarab/workflow/TransitionNode.java
trunk/src/webapp/images/assoc-first.gif (contents, props changed)
trunk/src/webapp/images/assoc-inter.gif (contents, props changed)
trunk/src/webapp/images/assoc-last.gif (contents, props changed)
trunk/src/webapp/images/assoc-pass-vertical.gif (contents, props changed)
trunk/src/webapp/images/assoc-single.gif (contents, props changed)
Modified:
trunk/src/java/org/tigris/scarab/tools/ScarabRequestTool.java
trunk/src/java/org/tigris/scarab/workflow/CheapWorkflow.java
trunk/src/java/org/tigris/scarab/workflow/DefaultWorkflow.java
trunk/src/java/org/tigris/scarab/workflow/Workflow.java
trunk/src/webapp/WEB-INF/templates/screens/home/EnterNew.vm
Log:
Added support for graphical display of transitions.
first usage in warning popup when creating of an issue is blocked
due to (bad) transition configuration:
In ScarabGlobalTool: new method getTransitionMatrix()
this method creates a list of rows containing workflow pathes.
The data is optimized for display purposes.
In Workflow added new method getTransitionTree()
This method returns the set of available transitions in form
of a tree starting at the "empty" to "something" transition
In screens/home/EnterNew.vm added renderer for transitionMatrix
Added new Class TransitionNode which makes up the TransitionTree.
it asically is a node which contains the fromOption and the list
of childnodes (TransitionNodes) representig all possible toOptions.
Added five image-snippets for path representation in the TransitionMatrix
Modified: trunk/src/java/org/tigris/scarab/tools/ScarabRequestTool.java
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/java/org/tigris/scarab/tools/ScarabRequestTool.java?view=diff&rev=10144&p1=trunk/src/java/org/tigris/scarab/tools/ScarabRequestTool.java&p2=trunk/src/java/org/tigris/scarab/tools/ScarabRequestTool.java&r1=10143&r2=10144
==============================================================================
--- trunk/src/java/org/tigris/scarab/tools/ScarabRequestTool.java (original)
+++ trunk/src/java/org/tigris/scarab/tools/ScarabRequestTool.java 2006-06-05 11:36:19-0700
@@ -147,6 +147,7 @@
import org.tigris.scarab.util.word.MaxConcurrentSearchException;
import org.tigris.scarab.util.word.QueryResult;
import org.tigris.scarab.util.word.SearchIndex;
+import org.tigris.scarab.workflow.TransitionNode;
import org.tigris.scarab.workflow.Workflow;
/**
@@ -3317,6 +3318,52 @@
return result;
}
+ /**
+ * Returns the list of transitions allowed for the current user
+ * in the current module/issueType/attribute combination as a displayable
+ * matrix organized in rows and columns.
+ * The returned list contains the matrix rows.
+ * Each row contains a set of OptionValues and OptionConnectors.
+ *
+ * An OptionConnector is a hint for a graphical representation and
+ * can be one of:
+ * <ul>
+ * <li>SINGLE or "---" </li>
+ * <li>FIRST or "-+-" </li>
+ * <li>INTERMEDIATE or " |-" </li>
+ * <li>LAST or " +-" </li>
+ * <li>PASSTHROUGH or " | " </li>
+ * </ul>
+ *
+ * A matrix-The may contain nullpointers if there is neither an
+ * OptionValue nor an OptionConnector associated to the cell. This
+ * can be seen best in an example (V* denotes OptionValues):
+ * <p><pre>
+ * [V1][---][V2][-+-][V3][-+-][V4]
+ * [ ][ ][ ][ | ][ ][ |-][V5]
+ * [ ][ ][ ][ | ][ ][ +-][V6]
+ * [ ][ ][ ][ +-][V7][ ][ ]
+ * </pre></p>
+ * If you remove the [] brackets, you should get the idea immedately:
+ * <p><pre>
+ * V1---V2-+-V3-+-V4
+ * | |-V5
+ * | +-V6
+ * +-V7
+ * </pre>
+ * </p>
+ * @throws TorqueException
+ */
+ public List getTransitionMatrix(IssueType issueType,
+ Attribute attribute) throws ScarabException
+ {
+ ScarabUser user = getCurrentUser();
+ Workflow workflow = ScarabGlobalTool.getWorkflow();
+ TransitionNode root = workflow.getTransitionTree(user,issueType,attribute);
+ List result = root.createRows();
+ return result;
+ }
+
// ****************** Recyclable implementation ************************
/**
Modified: trunk/src/java/org/tigris/scarab/workflow/CheapWorkflow.java
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/java/org/tigris/scarab/workflow/CheapWorkflow.java?view=diff&rev=10144&p1=trunk/src/java/org/tigris/scarab/workflow/CheapWorkflow.java&p2=trunk/src/java/org/tigris/scarab/workflow/CheapWorkflow.java&r1=10143&r2=10144
==============================================================================
--- trunk/src/java/org/tigris/scarab/workflow/CheapWorkflow.java (original)
+++ trunk/src/java/org/tigris/scarab/workflow/CheapWorkflow.java 2006-06-05 11:36:19-0700
@@ -146,19 +146,8 @@
{
Module module = user.getCurrentModule();
boolean result = false;
- List allTransitions = null;
- List availableOptions;
- try
- {
- availableOptions = module.getOptionTree(attribute,issueType,true);
- } catch (TorqueException e)
- {
- LocalizationKey key = L10NKeySet.ExceptionTorqueGeneric;
- L10NMessage msg = new L10NMessage(key,e);
- throw new ScarabException(msg,e);
- }
-
- allTransitions = TransitionPeer.getTransitionsFrom(availableOptions, attribute, fromOption);
+ List availableOptions = getAvailableOptions(issueType, attribute, module);
+ List allTransitions = TransitionPeer.getTransitionsFrom(availableOptions, attribute, fromOption);
Iterator iter = allTransitions.iterator();
if(!iter.hasNext())
{
@@ -197,18 +186,8 @@
Attribute attribute) throws ScarabException
{
Module module = user.getCurrentModule();
- List allTransitions = null;
- List availableOptions;
List result = null;
- try
- {
- availableOptions = module.getOptionTree(attribute,issueType,true);
- } catch (TorqueException e)
- {
- LocalizationKey key = L10NKeySet.ExceptionTorqueGeneric;
- L10NMessage msg = new L10NMessage(key,e);
- throw new ScarabException(msg,e);
- }
+ List availableOptions = getAvailableOptions(issueType, attribute, module);
Iterator optionsIter = availableOptions.iterator();
while(optionsIter.hasNext())
@@ -240,33 +219,156 @@
return result;
}
-
+ /**
+ * Returns the tree of transitions
+ * in the current module/issueType/attribute combination.
+ * @throws TorqueException
+ */
+ public TransitionNode getTransitionTree(ScarabUser user,
+ IssueType issueType,
+ Attribute attribute) throws ScarabException
+ {
+ Module module = user.getCurrentModule();
+ TransitionNode result = null;
+ List availableOptions = getAvailableOptions(issueType, attribute, module,false);
+ List visitedTransitions = null;
+ Iterator optionsIter = availableOptions.iterator();
+ while(optionsIter.hasNext())
+ {
+ RModuleOption rmoduleOption = (RModuleOption)optionsIter.next();
+ AttributeOption fromOption;
+ try
+ {
+ fromOption = rmoduleOption.getAttributeOption();
+ }
+ catch (TorqueException te)
+ {
+ L10NMessage msg = new L10NMessage(L10NKeySet.ExceptionTorqueGeneric,te);
+ throw new ScarabException(msg);
+ }
+ List list = getTransitionsFrom(user,issueType,attribute,fromOption,false);
+ if(list != null)
+ {
+ if(result == null)
+ {
+ result = new TransitionNode(fromOption);
+ }
+ for(int index=0; index < list.size(); index++)
+ {
+ Transition t = (Transition)list.get(index);
+ if(visitedTransitions == null)
+ {
+ visitedTransitions = new ArrayList();
+ }
+ if(visitedTransitions.contains(t))
+ {
+ continue;
+ }
+ visitedTransitions.add(t);
+ TransitionNode child = result.addNode(t);
+ getTransitionTree(user, issueType, attribute, t.getTo(), child, visitedTransitions);
+ }
+ }
+ }
+ return result;
+ }
+
/**
* Returns the list of transitions allowed for the current user
* in the current module/issueType/attribute combination
- * starting from fromOption.
* @throws TorqueException
*/
- public List getTransitionsFrom(ScarabUser user,
+ public void getTransitionTree(ScarabUser user,
IssueType issueType,
Attribute attribute,
- AttributeOption fromOption) throws ScarabException
+ AttributeOption fromOption,
+ TransitionNode node,
+ List visitedTransitions) throws ScarabException
+ {
+ List list = getTransitionsFrom(user,issueType,attribute,fromOption,false);
+ if(list != null)
+ {
+ for(int index=0; index < list.size(); index++)
+ {
+ Transition t = (Transition)list.get(index);
+ if(visitedTransitions.contains(t))
+ {
+ continue;
+ }
+ visitedTransitions.add(t);
+ TransitionNode child = node.addNode(t);
+ AttributeOption toOption = t.getTo();
+ TransitionNode parent = node.getParent();
+ while(parent != null && !parent.getOption().equals(toOption))
+ {
+ parent = parent.getParent();
+ }
+ if(parent == null)
+ {
+ getTransitionTree(user,issueType,attribute,t.getTo(),child,visitedTransitions);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param issueType
+ * @param attribute
+ * @param module
+ * @return
+ * @throws ScarabException
+ */
+ private List getAvailableOptions(IssueType issueType, Attribute attribute, Module module) throws ScarabException
+ {
+ return getAvailableOptions(issueType, attribute, module, true);
+ }
+ /**
+ * @param issueType
+ * @param attribute
+ * @param module
+ * @return
+ * @throws ScarabException
+ */
+ private List getAvailableOptions(IssueType issueType, Attribute attribute, Module module, boolean activeOnly) throws ScarabException
{
- Module module = user.getCurrentModule();
- List allTransitions = null;
List availableOptions;
- List result;
try
{
- availableOptions = module.getOptionTree(attribute,issueType,true);
+ availableOptions = module.getOptionTree(attribute,issueType,activeOnly);
} catch (TorqueException e)
{
LocalizationKey key = L10NKeySet.ExceptionTorqueGeneric;
L10NMessage msg = new L10NMessage(key,e);
throw new ScarabException(msg,e);
}
-
- allTransitions = TransitionPeer.getTransitionsFrom(availableOptions, attribute, fromOption);
+ return availableOptions;
+ }
+
+
+ /**
+ * Returns the list of transitions allowed for the current user
+ * in the current module/issueType/attribute combination
+ * starting from fromOption.
+ * @throws TorqueException
+ */
+ public List getTransitionsFrom(ScarabUser user,
+ IssueType issueType,
+ Attribute attribute,
+ AttributeOption fromOption) throws ScarabException
+ {
+ return getTransitionsFrom(user, issueType, attribute, fromOption, false);
+ }
+
+ private List getTransitionsFrom(ScarabUser user,
+ IssueType issueType,
+ Attribute attribute,
+ AttributeOption fromOption,
+ boolean activeOnly) throws ScarabException
+ {
+ Module module = user.getCurrentModule();
+ List result;
+ List availableOptions = getAvailableOptions(issueType, attribute, module, activeOnly);
+ List allTransitions = TransitionPeer.getTransitionsFrom(availableOptions, attribute, fromOption);
Iterator iter = allTransitions.iterator();
if(iter.hasNext())
{
@@ -276,21 +378,31 @@
Object obj = iter.next();
Transition transition = (Transition) obj;
- if (transitionIsSupportedByOptions(transition, availableOptions))
+ boolean isSupportedByOptions = transitionIsSupportedByOptions(transition, availableOptions);
+ if (isSupportedByOptions || !activeOnly)
{
- boolean transitionAllowed = false;
- Role requiredRole = transition.getRole();
- if (requiredRole != null)
- {
- // A role is required for this transition to be allowed
- transitionAllowed =
- user.hasRoleInModule(requiredRole, module);
+ boolean addTransition;
+
+ if(activeOnly)
+ {
+ Role requiredRole = transition.getRole();
+ if (requiredRole != null)
+ {
+ // A role is required for this transition to be allowed
+ addTransition = user.hasRoleInModule(
+ requiredRole, module);
+ }
+ else
+ {
+ addTransition = true;
+ }
}
else
{
- transitionAllowed = true;
+ addTransition = true;
}
- if (transitionAllowed)
+
+ if (addTransition)
{
result.add(transition);
}
Modified: trunk/src/java/org/tigris/scarab/workflow/DefaultWorkflow.java
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/java/org/tigris/scarab/workflow/DefaultWorkflow.java?view=diff&rev=10144&p1=trunk/src/java/org/tigris/scarab/workflow/DefaultWorkflow.java&p2=trunk/src/java/org/tigris/scarab/workflow/DefaultWorkflow.java&r1=10143&r2=10144
==============================================================================
--- trunk/src/java/org/tigris/scarab/workflow/DefaultWorkflow.java (original)
+++ trunk/src/java/org/tigris/scarab/workflow/DefaultWorkflow.java 2006-06-05 11:36:19-0700
@@ -92,7 +92,17 @@
return null;
}
-
+ /**
+ * Returns the tree of transitions
+ * in the current module/issueType/attribute combination.
+ * @throws TorqueException
+ */
+ public TransitionNode getTransitionTree(ScarabUser user,
+ IssueType issueType,
+ Attribute attribute) throws ScarabException
+ {
+ return null;
+ }
/**
* Returns the list of transitions allowed for the current user
* in the current module/issueType/attribute combination
Added: trunk/src/java/org/tigris/scarab/workflow/TransitionNode.java
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/java/org/tigris/scarab/workflow/TransitionNode.java?view=auto&rev=10144
==============================================================================
--- (empty file)
+++ trunk/src/java/org/tigris/scarab/workflow/TransitionNode.java 2006-06-05 11:36:19-0700
@@ -0,0 +1,220 @@
+package org.tigris.scarab.workflow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.tigris.scarab.om.AttributeOption;
+import org.tigris.scarab.om.Transition;
+
+/* ====================================================================
+*
+* Copyright (c) 2006 CollabNet.
+*
+* Licensed under the
+*
+* CollabNet/Tigris.org Apache-style license (the "License");
+*
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://scarab.tigris.org/LICENSE
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+* implied. See the License for the specific language governing
+* permissions and limitations under the License.
+* ====================================================================
+*
+* This software consists of voluntary contributions made by many
+* individuals on behalf of CollabNet.
+*
+*
+*/
+
+/**
+ * The Transitions available for Attributes span a recursive TransitionTree.
+ * A TransitionNode is one node in the TransitionTree. It contains a pointer
+ * to a source AttributeOption of a Transition and a List of TransitionNodes
+ * which contain target AttributeOptions which can be reached from
+ * this AttributeOption.
+ * @author [email protected]
+ */
+
+public class TransitionNode
+{
+ private AttributeOption fromOption;
+ List children;
+ TransitionNode parent;
+
+ static final Integer BLANK = new Integer(0);
+ static final Integer SINGLE = new Integer(1);
+ static final Integer FIRST = new Integer(2);
+ static final Integer INTER = new Integer(3);
+ static final Integer LAST = new Integer(4);
+ static final Integer PASS = new Integer(5);
+
+ TransitionNode(AttributeOption option)
+ {
+ fromOption = option;
+ children = new ArrayList();
+ parent = null;
+ }
+
+ public void addNode(TransitionNode node)
+ {
+ children.add(node);
+ node.setParent(this);
+ }
+
+
+ public TransitionNode addNode(Transition transition)
+ {
+ AttributeOption toOption = transition.getTo();
+ TransitionNode child = new TransitionNode(toOption);
+ addNode(child);
+ return child;
+ }
+
+ private void setParent(TransitionNode node)
+ {
+ parent = node;
+ }
+
+ public TransitionNode getParent()
+ {
+ return parent;
+ }
+
+ public AttributeOption getOption()
+ {
+ return fromOption;
+ }
+
+ public List getChildren()
+ {
+ return children;
+ }
+
+ public List createRows()
+ {
+ List rows = new ArrayList();
+ List currentRow = new ArrayList();
+ return fillRows(rows, currentRow);
+ }
+
+ private List fillRows(List rows, List currentRow)
+ {
+ currentRow.add(getOption());
+ if(children.size() > 0)
+ {
+ int size = children.size();
+ for(int index=0; index < size; index++)
+ {
+ TransitionNode child = (TransitionNode)children.get(index);
+ if (size == 1)
+ {
+ currentRow.add(SINGLE);
+ }
+ else
+ {
+ if (index==0)
+ {
+ currentRow.add(FIRST);
+ }
+ else if (index == size-1)
+ {
+ currentRow.add(LAST);
+ }
+ else
+ {
+ currentRow.add(INTER);
+ }
+ }
+ child.fillRows(rows, currentRow);
+ if(index < (size-1))
+ {
+ currentRow = createNewRow();
+ }
+ }
+ }
+ else
+ {
+ rows.add(currentRow);
+ }
+ return rows;
+ }
+
+ private List createNewRow()
+ {
+ List result;
+ TransitionNode parent = getParent();
+ if(parent != null)
+ {
+ result = parent.createNewRow();
+ Integer type = getConnectionType();
+ result.add(type);
+ }
+ else
+ {
+ result = new ArrayList();
+ }
+ result.add("");
+ return result;
+ }
+
+ private Integer getConnectionType()
+ {
+ TransitionNode parent = getParent();
+ int parentSize = parent.getChildren().size();
+ Integer result;
+
+ if(parentSize == 1)
+ {
+ result = BLANK;
+ }
+ else
+ {
+ int index = 0;
+ for (index = 0; index < parentSize; index++)
+ {
+ TransitionNode node = (TransitionNode) parent.getChildren()
+ .get(index);
+ if (node == this)
+ {
+ break;
+ }
+ }
+
+ if (index == 0)
+ {
+ result = PASS;
+ }
+ else if (index == (parentSize - 1))
+ {
+ result = BLANK;
+ }
+ else
+ {
+ result = PASS;
+ }
+ }
+ return result;
+ }
+
+ public int getTreeDepth()
+ {
+ int result = 0;
+ for(int index=0; index < children.size(); index++)
+ {
+ TransitionNode child = (TransitionNode)children.get(index);
+ int childDepth = child.getTreeDepth();
+ if(childDepth >= result)
+ {
+ result = childDepth + 1;
+ }
+ }
+ return result;
+ }
+
+}
Modified: trunk/src/java/org/tigris/scarab/workflow/Workflow.java
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/java/org/tigris/scarab/workflow/Workflow.java?view=diff&rev=10144&p1=trunk/src/java/org/tigris/scarab/workflow/Workflow.java&p2=trunk/src/java/org/tigris/scarab/workflow/Workflow.java&r1=10143&r2=10144
==============================================================================
--- trunk/src/java/org/tigris/scarab/workflow/Workflow.java (original)
+++ trunk/src/java/org/tigris/scarab/workflow/Workflow.java 2006-06-05 11:36:19-0700
@@ -87,6 +87,15 @@
IssueType issueType,
Attribute attribute) throws ScarabException;
+ /**
+ * Returns the tree of transitions
+ * in the current module/issueType/attribute combination.
+ * @throws TorqueException
+ */
+ public TransitionNode getTransitionTree(ScarabUser user,
+ IssueType issueType,
+ Attribute attribute) throws ScarabException;
+
/**
* Returns the list of transitions allowed for the current user
Modified: trunk/src/webapp/WEB-INF/templates/screens/home/EnterNew.vm
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/webapp/WEB-INF/templates/screens/home/EnterNew.vm?view=diff&rev=10144&p1=trunk/src/webapp/WEB-INF/templates/screens/home/EnterNew.vm&p2=trunk/src/webapp/WEB-INF/templates/screens/home/EnterNew.vm&r1=10143&r2=10144
==============================================================================
--- trunk/src/webapp/WEB-INF/templates/screens/home/EnterNew.vm (original)
+++ trunk/src/webapp/WEB-INF/templates/screens/home/EnterNew.vm 2006-06-05 11:36:19-0700
@@ -35,17 +35,13 @@
style="cursor:pointer"/>
- <div style="width:500px;" class="scarab-tooltip" id="$rmit.DisplayName" onclick="hideTooltip()">
- <p>
- You have not the permission to create "$rmit.DisplayName" - Issues.
- The following table shows you the list required attributes for which
- you do not have access permission:
- </p>
+ <div style="width:600px;" class="scarab-tooltip" id="$rmit.DisplayName" onclick="hideTooltip()">
+ <p>$l10n.NoCreateTransition</p>
<table>
#set ($attributeList = $issueType.getRequiredAttributes($module))
<tr>
- <th>required attribute</th>
- <th>access</th>
+ <th>$l10n.RequiredAttribute</th>
+ <th>$l10n.AllowedTransitions</th>
</tr>
#foreach ($attribute in $attributeList)
#set ($allowed = $issueType.canCreateAttributeInScope($user, $module, $attribute) )
@@ -53,25 +49,32 @@
#set ($checkmark = "/images/icon_stop.gif" )
<tr>
<td><a href="$staticLink.setPath("/issues/template/admin%2CGlobalAttributeEdit.vm/att_0id/$attribute.attributeId")">$attribute.name</a></td>
- <td><img src="$staticLink.setPath($checkmark)" alt="$allowed" border="0"</td>
+ <td class="graphic">
+ <table class="graphic">
+ #set ( $rows = $scarabR.getTransitionMatrix($issueType,$attribute))
+ #set( $img = ["", "assoc-single.gif", "assoc-first.gif", "assoc-inter.gif" ,"assoc-last.gif", "assoc-pass-vertical.gif"] )
+ #foreach ($row in $rows)
+ <tr>
+ #foreach ($item in $row)
+ #if ( ($velocityCount % 2) == 1)
+ <td>#if(!($item.equals(""))) $item.name #end</td>
+ #else
+ <td><img align="middle" border="0" src="$staticLink.setPath("/images/$img.get($item)")"></td>
+ #end
+ #end
+ <!--td>$transition.fromName</td>
+ <td><img align="middle" border="0" src="$staticLink.setPath("/images/assoc-inter.gif")"></td>
+ <td>$transition.toName</td-->
+ </tr>
+ #end
+ </table>
+ </td>
</tr>
#end
#end
</table>
- <p>
- If you think, this is an error, please contact your Scarab Administrator.
- Probably your admin has defined some transition rules, which prevent
- you from creating the listed attributes. This typically happens,
- when the current module does not use all defined option values
- of an attribute and all transitions you are allowed to set are
- associated to these non used option values.
- </p>
- <p>
- Please check carefully your transition matrix
- by following the link presented in the table above
- and scroll to the bottom of the target page.
- </p>
-
+ <p>$l10n.CreateTransitionHelp1</p>
+ <p>$l10n.CreateTransitionHelp2</p>
</div>
#end
Added: trunk/src/webapp/images/assoc-first.gif
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/webapp/images/assoc-first.gif?view=auto&rev=10144
==============================================================================
Binary file. No diff available.
Added: trunk/src/webapp/images/assoc-inter.gif
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/webapp/images/assoc-inter.gif?view=auto&rev=10144
==============================================================================
Binary file. No diff available.
Added: trunk/src/webapp/images/assoc-last.gif
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/webapp/images/assoc-last.gif?view=auto&rev=10144
==============================================================================
Binary file. No diff available.
Added: trunk/src/webapp/images/assoc-pass-vertical.gif
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/webapp/images/assoc-pass-vertical.gif?view=auto&rev=10144
==============================================================================
Binary file. No diff available.
Added: trunk/src/webapp/images/assoc-single.gif
Url: http://scarab.tigris.org/source/browse/scarab/trunk/src/webapp/images/assoc-single.gif?view=auto&rev=10144
==============================================================================
Binary file. No diff available.
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.