From: <lor...@us...> - 2011-08-04 08:38:29
|
Revision: 2985 http://dl-learner.svn.sourceforge.net/dl-learner/?rev=2985&view=rev Author: lorenz_b Date: 2011-08-04 08:38:23 +0000 (Thu, 04 Aug 2011) Log Message: ----------- Added Test class. Continued learning algorithms. Fixed config helper. Modified Paths: -------------- trunk/components-core/src/main/java/org/dllearner/algorithms/properties/FunctionalPropertyAxiomLearner.java trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyDomainAxiomLearner.java trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyRangeAxiomLearner.java trunk/components-core/src/main/java/org/dllearner/algorithms/properties/ReflexivePropertyAxiomLearner.java trunk/components-core/src/main/java/org/dllearner/algorithms/properties/SymmetricPropertyAxiomLearner.java trunk/components-core/src/main/java/org/dllearner/core/config/ConfigHelper.java Added Paths: ----------- trunk/components-core/src/test/java/org/dllearner/test/junit/PropertyLearningTest.java Modified: trunk/components-core/src/main/java/org/dllearner/algorithms/properties/FunctionalPropertyAxiomLearner.java =================================================================== --- trunk/components-core/src/main/java/org/dllearner/algorithms/properties/FunctionalPropertyAxiomLearner.java 2011-08-04 08:35:37 UTC (rev 2984) +++ trunk/components-core/src/main/java/org/dllearner/algorithms/properties/FunctionalPropertyAxiomLearner.java 2011-08-04 08:38:23 UTC (rev 2985) @@ -28,7 +28,7 @@ public class FunctionalPropertyAxiomLearner extends Component implements AxiomLearningAlgorithm { -private static final Logger logger = LoggerFactory.getLogger(TransitivePropertyAxiomLearner.class); + private static final Logger logger = LoggerFactory.getLogger(FunctionalPropertyAxiomLearner.class); @ConfigOption(name="propertyToDescribe", description="", propertyEditorClass=ObjectPropertyEditor.class) private ObjectProperty propertyToDescribe; @@ -87,19 +87,26 @@ logger.info("Property is already declared as functional in knowledge base."); } - //get fraction of instances s with <s p o> also exists <o p s> - query = "SELECT (COUNT(?s)) AS ?all ,(COUNT(?o1)) AS ?functional WHERE {?s <%s> ?o. OPTIONAL{?o <%s> ?s. ?o <%s> ?o1}}"; - query = query.replace("%s", propertyToDescribe.getURI().toString()); + //get number of instances of s with <s p o> + query = String.format("SELECT (COUNT(DISTINCT ?s)) AS ?all WHERE {?s <%s> ?o.}", propertyToDescribe.getName()); ResultSet rs = executeQuery(query); QuerySolution qs; + int all = 1; while(rs.hasNext()){ qs = rs.next(); - int all = qs.getLiteral("all").getInt(); - int symmetric = qs.getLiteral("functional").getInt(); - double frac = symmetric / (double)all; - currentlyBestAxioms.add(new EvaluatedAxiom(new FunctionalObjectPropertyAxiom(propertyToDescribe), new AxiomScore(frac))); + all = qs.getLiteral("all").getInt(); } - + //get number of instances of s with <s p o> <s p o1> where o != o1 + query = "SELECT (COUNT(DISTINCT ?s)) AS ?notfunctional WHERE {?s <%s> ?o. ?s <%s> ?o1. FILTER(?o != ?o1) }"; + query = query.replace("%s", propertyToDescribe.getURI().toString()); + rs = executeQuery(query); + int notFunctional = 1; + while(rs.hasNext()){ + qs = rs.next(); + notFunctional = qs.getLiteral("notfunctional").getInt(); + } + double frac = (all - notFunctional) / (double)all; + currentlyBestAxioms.add(new EvaluatedAxiom(new FunctionalObjectPropertyAxiom(propertyToDescribe), new AxiomScore(frac))); logger.info("...finished in {}ms.", (System.currentTimeMillis()-startTime)); } Modified: trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyDomainAxiomLearner.java =================================================================== --- trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyDomainAxiomLearner.java 2011-08-04 08:35:37 UTC (rev 2984) +++ trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyDomainAxiomLearner.java 2011-08-04 08:38:23 UTC (rev 2985) @@ -18,7 +18,6 @@ import org.dllearner.core.Component; import org.dllearner.core.ComponentInitException; import org.dllearner.core.EvaluatedAxiom; -import org.dllearner.core.config.ConfigHelper; import org.dllearner.core.config.ConfigOption; import org.dllearner.core.config.IntegerEditor; import org.dllearner.core.config.ObjectPropertyEditor; @@ -122,7 +121,7 @@ public List<EvaluatedAxiom> getCurrentlyBestEvaluatedAxioms(int nrOfAxioms) { int max = Math.min(currentlyBestAxioms.size(), nrOfAxioms); - List<EvaluatedAxiom> bestAxioms = currentlyBestAxioms.subList(0, max-1); + List<EvaluatedAxiom> bestAxioms = currentlyBestAxioms.subList(0, max); return bestAxioms; } @@ -229,9 +228,9 @@ public static void main(String[] args) throws Exception{ Map<String, String> propertiesMap = new HashMap<String, String>(); - propertiesMap.put("propertyToDescribe", "http://dbpedia.org/ontology/league"); - propertiesMap.put("maxExecutionTimeInSeconds", "20"); - propertiesMap.put("maxFetchedRows", "5000"); + propertiesMap.put("propertyToDescribe", "http://dbpedia.org/ontology/writer"); + propertiesMap.put("maxExecutionTimeInSeconds", "10"); + propertiesMap.put("maxFetchedRows", "15000"); PropertyDomainAxiomLearner l = new PropertyDomainAxiomLearner(new SparqlEndpointKS(SparqlEndpoint.getEndpointDBpedia())); @@ -246,7 +245,6 @@ f.set(l, editor.getValue()); } } - ConfigHelper.configure(l, "propertyToDescribe", "test"); l.init(); l.start(); System.out.println(l.getCurrentlyBestEvaluatedAxioms(3)); Modified: trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyRangeAxiomLearner.java =================================================================== --- trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyRangeAxiomLearner.java 2011-08-04 08:35:37 UTC (rev 2984) +++ trunk/components-core/src/main/java/org/dllearner/algorithms/properties/PropertyRangeAxiomLearner.java 2011-08-04 08:38:23 UTC (rev 2985) @@ -1,42 +1,127 @@ package org.dllearner.algorithms.properties; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import org.dllearner.core.AxiomLearningAlgorithm; import org.dllearner.core.Component; import org.dllearner.core.ComponentInitException; import org.dllearner.core.EvaluatedAxiom; +import org.dllearner.core.config.ConfigOption; +import org.dllearner.core.config.IntegerEditor; +import org.dllearner.core.config.ObjectPropertyEditor; import org.dllearner.core.configurators.Configurator; import org.dllearner.core.owl.Axiom; +import org.dllearner.core.owl.Description; +import org.dllearner.core.owl.Individual; +import org.dllearner.core.owl.NamedClass; +import org.dllearner.core.owl.ObjectProperty; +import org.dllearner.core.owl.ObjectPropertyRangeAxiom; import org.dllearner.kb.SparqlEndpointKS; +import org.dllearner.learningproblems.AxiomScore; +import org.dllearner.reasoning.SPARQLReasoner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.hp.hpl.jena.query.QuerySolution; +import com.hp.hpl.jena.query.ResultSet; +import com.hp.hpl.jena.sparql.engine.http.QueryEngineHTTP; + public class PropertyRangeAxiomLearner extends Component implements AxiomLearningAlgorithm { - private String propertyToDescribe; +private static final Logger logger = LoggerFactory.getLogger(PropertyRangeAxiomLearner.class); - public String getPropertyToDescribe() { + @ConfigOption(name="propertyToDescribe", description="", propertyEditorClass=ObjectPropertyEditor.class) + private ObjectProperty propertyToDescribe; + @ConfigOption(name="maxExecutionTimeInSeconds", description="", propertyEditorClass=IntegerEditor.class) + private int maxExecutionTimeInSeconds = 10; + @ConfigOption(name="maxFetchedRows", description="The maximum number of rows fetched from the endpoint to approximate the result.", propertyEditorClass=IntegerEditor.class) + private int maxFetchedRows = 0; + + private SPARQLReasoner reasoner; + private SparqlEndpointKS ks; + + private List<EvaluatedAxiom> currentlyBestAxioms; + private long startTime; + private int fetchedRows; + + public PropertyRangeAxiomLearner(SparqlEndpointKS ks){ + this.ks = ks; + } + + public int getMaxExecutionTimeInSeconds() { + return maxExecutionTimeInSeconds; + } + + public void setMaxExecutionTimeInSeconds(int maxExecutionTimeInSeconds) { + this.maxExecutionTimeInSeconds = maxExecutionTimeInSeconds; + } + + public ObjectProperty getPropertyToDescribe() { return propertyToDescribe; } - public void setPropertyToDescribe(String propertyToDescribe) { + public void setPropertyToDescribe(ObjectProperty propertyToDescribe) { this.propertyToDescribe = propertyToDescribe; } + + public int getMaxFetchedRows() { + return maxFetchedRows; + } - public PropertyRangeAxiomLearner(SparqlEndpointKS ks){ - + public void setMaxFetchedRows(int maxFetchedRows) { + this.maxFetchedRows = maxFetchedRows; } - + @Override public void start() { - // TODO Auto-generated method stub - + logger.info("Start learning..."); + startTime = System.currentTimeMillis(); + fetchedRows = 0; + currentlyBestAxioms = new ArrayList<EvaluatedAxiom>(); + //get existing range + Description existingRange = reasoner.getRange(propertyToDescribe); + logger.debug("Existing range: " + existingRange); + + //get objects with types + Map<Individual, Set<NamedClass>> individual2Types = new HashMap<Individual, Set<NamedClass>>(); + while(!terminationCriteriaSatisfied()){ + individual2Types.putAll(getObjectsWithTypes(fetchedRows)); + currentlyBestAxioms = buildBestAxioms(individual2Types); + fetchedRows += 1000; + } + logger.info("...finished in {}ms.", (System.currentTimeMillis()-startTime)); } @Override public List<Axiom> getCurrentlyBestAxioms(int nrOfAxioms) { - // TODO Auto-generated method stub - return null; + List<Axiom> bestAxioms = new ArrayList<Axiom>(); + + Iterator<EvaluatedAxiom> it = currentlyBestAxioms.iterator(); + while(bestAxioms.size() < nrOfAxioms && it.hasNext()){ + bestAxioms.add(it.next().getAxiom()); + } + + return bestAxioms; } + + @Override + public List<EvaluatedAxiom> getCurrentlyBestEvaluatedAxioms(int nrOfAxioms) { + int max = Math.min(currentlyBestAxioms.size(), nrOfAxioms); + + List<EvaluatedAxiom> bestAxioms = currentlyBestAxioms.subList(0, max); + + return bestAxioms; + } @Override public Configurator getConfigurator() { @@ -46,14 +131,96 @@ @Override public void init() throws ComponentInitException { - // TODO Auto-generated method stub + reasoner = new SPARQLReasoner(ks); } + + private boolean terminationCriteriaSatisfied(){ + boolean timeLimitExceeded = maxExecutionTimeInSeconds == 0 ? false : (System.currentTimeMillis() - startTime) >= maxExecutionTimeInSeconds * 1000; + boolean resultLimitExceeded = maxFetchedRows == 0 ? false : fetchedRows >= maxFetchedRows; + return timeLimitExceeded || resultLimitExceeded; + } + + private List<EvaluatedAxiom> buildBestAxioms(Map<Individual, Set<NamedClass>> individual2Types){ + List<EvaluatedAxiom> axioms = new ArrayList<EvaluatedAxiom>(); + Map<NamedClass, Integer> result = new HashMap<NamedClass, Integer>(); + for(Entry<Individual, Set<NamedClass>> entry : individual2Types.entrySet()){ + for(NamedClass nc : entry.getValue()){ + Integer cnt = result.get(nc); + if(cnt == null){ + cnt = Integer.valueOf(1); + } + result.put(nc, Integer.valueOf(cnt + 1)); + } + } + + EvaluatedAxiom evalAxiom; + for(Entry<NamedClass, Integer> entry : sortByValues(result)){ + evalAxiom = new EvaluatedAxiom(new ObjectPropertyRangeAxiom(propertyToDescribe, entry.getKey()), + new AxiomScore(entry.getValue() / (double)individual2Types.keySet().size())); + axioms.add(evalAxiom); + } + + return axioms; + } + + /* + * Returns the entries of the map sorted by value. + */ + private SortedSet<Entry<NamedClass, Integer>> sortByValues(Map<NamedClass, Integer> map){ + SortedSet<Entry<NamedClass, Integer>> sortedSet = new TreeSet<Map.Entry<NamedClass,Integer>>(new Comparator<Entry<NamedClass, Integer>>() { - @Override - public List<EvaluatedAxiom> getCurrentlyBestEvaluatedAxioms(int nrOfAxioms) { - // TODO Auto-generated method stub - return null; + @Override + public int compare(Entry<NamedClass, Integer> value1, Entry<NamedClass, Integer> value2) { + if(value1.getValue() < value2.getValue()){ + return 1; + } else if(value2.getValue() < value1.getValue()){ + return -1; + } else { + return value1.getKey().compareTo(value2.getKey()); + } + } + }); + sortedSet.addAll(map.entrySet()); + return sortedSet; } + + private Map<Individual, Set<NamedClass>> getObjectsWithTypes(int offset){ + Map<Individual, Set<NamedClass>> individual2Types = new HashMap<Individual, Set<NamedClass>>(); + int limit = 1000; + String query = String.format("SELECT ?ind ?type WHERE {?s <%s> ?ind. ?ind a ?type.} LIMIT %d OFFSET %d", propertyToDescribe.getName(), limit, offset); + ResultSet rs = executeQuery(query); + QuerySolution qs; + Individual ind; + Set<NamedClass> types; + while(rs.hasNext()){ + qs = rs.next(); + ind = new Individual(qs.getResource("ind").getURI()); + types = individual2Types.get(ind); + if(types == null){ + types = new HashSet<NamedClass>(); + individual2Types.put(ind, types); + } + types.add(new NamedClass(qs.getResource("type").getURI())); + } + return individual2Types; + } + + /* + * Executes a SELECT query and returns the result. + */ + private ResultSet executeQuery(String query){ + logger.info("Sending query \n {}", query); + + QueryEngineHTTP queryExecution = new QueryEngineHTTP(ks.getEndpoint().getURL().toString(), query); + for (String dgu : ks.getEndpoint().getDefaultGraphURIs()) { + queryExecution.addDefaultGraph(dgu); + } + for (String ngu : ks.getEndpoint().getNamedGraphURIs()) { + queryExecution.addNamedGraph(ngu); + } + ResultSet resultSet = queryExecution.execSelect(); + return resultSet; + } } Modified: trunk/components-core/src/main/java/org/dllearner/algorithms/properties/ReflexivePropertyAxiomLearner.java =================================================================== --- trunk/components-core/src/main/java/org/dllearner/algorithms/properties/ReflexivePropertyAxiomLearner.java 2011-08-04 08:35:37 UTC (rev 2984) +++ trunk/components-core/src/main/java/org/dllearner/algorithms/properties/ReflexivePropertyAxiomLearner.java 2011-08-04 08:38:23 UTC (rev 2985) @@ -28,7 +28,7 @@ public class ReflexivePropertyAxiomLearner extends Component implements AxiomLearningAlgorithm { - private static final Logger logger = LoggerFactory.getLogger(TransitivePropertyAxiomLearner.class); + private static final Logger logger = LoggerFactory.getLogger(ReflexivePropertyAxiomLearner.class); @ConfigOption(name="propertyToDescribe", description="", propertyEditorClass=ObjectPropertyEditor.class) private ObjectProperty propertyToDescribe; Modified: trunk/components-core/src/main/java/org/dllearner/algorithms/properties/SymmetricPropertyAxiomLearner.java =================================================================== --- trunk/components-core/src/main/java/org/dllearner/algorithms/properties/SymmetricPropertyAxiomLearner.java 2011-08-04 08:35:37 UTC (rev 2984) +++ trunk/components-core/src/main/java/org/dllearner/algorithms/properties/SymmetricPropertyAxiomLearner.java 2011-08-04 08:38:23 UTC (rev 2985) @@ -14,7 +14,6 @@ import org.dllearner.core.configurators.Configurator; import org.dllearner.core.owl.Axiom; import org.dllearner.core.owl.ObjectProperty; -import org.dllearner.core.owl.ReflexiveObjectPropertyAxiom; import org.dllearner.core.owl.SymmetricObjectPropertyAxiom; import org.dllearner.kb.SparqlEndpointKS; import org.dllearner.learningproblems.AxiomScore; @@ -29,7 +28,7 @@ public class SymmetricPropertyAxiomLearner extends Component implements AxiomLearningAlgorithm { - private static final Logger logger = LoggerFactory.getLogger(TransitivePropertyAxiomLearner.class); + private static final Logger logger = LoggerFactory.getLogger(SymmetricPropertyAxiomLearner.class); @ConfigOption(name="propertyToDescribe", description="", propertyEditorClass=ObjectPropertyEditor.class) private ObjectProperty propertyToDescribe; Modified: trunk/components-core/src/main/java/org/dllearner/core/config/ConfigHelper.java =================================================================== --- trunk/components-core/src/main/java/org/dllearner/core/config/ConfigHelper.java 2011-08-04 08:35:37 UTC (rev 2984) +++ trunk/components-core/src/main/java/org/dllearner/core/config/ConfigHelper.java 2011-08-04 08:38:23 UTC (rev 2985) @@ -1,15 +1,32 @@ package org.dllearner.core.config; import java.beans.PropertyEditor; -import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.dllearner.algorithms.properties.PropertyDomainAxiomLearner; import org.dllearner.core.Component; public class ConfigHelper { + public final static Map<Class<?>, Class<?>> map = new HashMap<Class<?>, Class<?>>(); + + static { + map.put(Boolean.class, boolean.class); + map.put(Byte.class, byte.class); + map.put(Short.class, short.class); + map.put(Character.class, char.class); + map.put(Integer.class, int.class); + map.put(Long.class, long.class); + map.put(Float.class, float.class); + map.put(Double.class, double.class); + } + /** * Configures the given component by setting the value for the appropriate config option. * @param component the component to be configured @@ -25,13 +42,20 @@ try { PropertyEditor editor = (PropertyEditor) option.propertyEditorClass().newInstance(); editor.setAsText(configValue); - f.set(component, editor.getValue()); + Method method = component.getClass().getMethod("set" + Character.toUpperCase(f.getName().charAt(0)) + f.getName().substring(1), getClassForObject(editor.getValue())); + method.invoke(component, editor.getValue()); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); } } @@ -57,5 +81,19 @@ return options; } + + private static Class<?> getClassForObject(Object obj){ + if(map.containsKey(obj.getClass())){ + return map.get(obj.getClass()); + } else { + return obj.getClass(); + } + } + + public static void main(String[] args) { + PropertyDomainAxiomLearner l = new PropertyDomainAxiomLearner(null); + ConfigHelper.configure(l, "maxExecutionTimeInSeconds", "11"); + System.out.println(l.getMaxExecutionTimeInSeconds()); + } } Added: trunk/components-core/src/test/java/org/dllearner/test/junit/PropertyLearningTest.java =================================================================== --- trunk/components-core/src/test/java/org/dllearner/test/junit/PropertyLearningTest.java (rev 0) +++ trunk/components-core/src/test/java/org/dllearner/test/junit/PropertyLearningTest.java 2011-08-04 08:38:23 UTC (rev 2985) @@ -0,0 +1,78 @@ +package org.dllearner.test.junit; + +import org.dllearner.algorithms.properties.FunctionalPropertyAxiomLearner; +import org.dllearner.algorithms.properties.PropertyDomainAxiomLearner; +import org.dllearner.algorithms.properties.PropertyRangeAxiomLearner; +import org.dllearner.algorithms.properties.ReflexivePropertyAxiomLearner; +import org.dllearner.algorithms.properties.SymmetricPropertyAxiomLearner; +import org.dllearner.core.owl.ObjectProperty; +import org.dllearner.kb.SparqlEndpointKS; +import org.dllearner.kb.sparql.SparqlEndpoint; + +import junit.framework.TestCase; + +public class PropertyLearningTest extends TestCase{ + + private SparqlEndpointKS ks; + private int maxExecutionTimeInSeconds = 5; + private int nrOfAxioms = 3; + + private ObjectProperty functional = new ObjectProperty("http://dbpedia.org/ontology/league"); + private ObjectProperty reflexive = new ObjectProperty("http://dbpedia.org/ontology/influencedBy"); + private ObjectProperty symmetric = new ObjectProperty("http://dbpedia.org/ontology/influencedBy"); + private ObjectProperty domain = new ObjectProperty("http://dbpedia.org/ontology/writer"); + private ObjectProperty range = new ObjectProperty("http://dbpedia.org/ontology/writer"); + + + @Override + protected void setUp() throws Exception { + super.setUp(); + ks = new SparqlEndpointKS(SparqlEndpoint.getEndpointDBpedia()); + } + + public void testPropertyDomainAxiomLearning() throws Exception { + PropertyDomainAxiomLearner l = new PropertyDomainAxiomLearner(ks); + l.setMaxExecutionTimeInSeconds(maxExecutionTimeInSeconds); + l.setPropertyToDescribe(domain); + l.init(); + l.start(); + System.out.println(l.getCurrentlyBestEvaluatedAxioms(nrOfAxioms)); + } + + public void testPropertyRangeAxiomLearning() throws Exception { + PropertyRangeAxiomLearner l = new PropertyRangeAxiomLearner(ks); + l.setMaxExecutionTimeInSeconds(maxExecutionTimeInSeconds); + l.setPropertyToDescribe(range); + l.init(); + l.start(); + System.out.println(l.getCurrentlyBestEvaluatedAxioms(nrOfAxioms)); + } + + public void testReflexivePropertyAxiomLearning() throws Exception { + ReflexivePropertyAxiomLearner l = new ReflexivePropertyAxiomLearner(ks); + l.setMaxExecutionTimeInSeconds(maxExecutionTimeInSeconds); + l.setPropertyToDescribe(reflexive); + l.init(); + l.start(); + System.out.println(l.getCurrentlyBestEvaluatedAxioms(nrOfAxioms)); + } + + public void testFunctionalPropertyAxiomLearnining() throws Exception { + FunctionalPropertyAxiomLearner l = new FunctionalPropertyAxiomLearner(ks); + l.setMaxExecutionTimeInSeconds(maxExecutionTimeInSeconds); + l.setPropertyToDescribe(functional); + l.init(); + l.start(); + System.out.println(l.getCurrentlyBestEvaluatedAxioms(nrOfAxioms)); + } + + public void testSymmetricPropertyAxiomLearning() throws Exception { + SymmetricPropertyAxiomLearner l = new SymmetricPropertyAxiomLearner(ks); + l.setMaxExecutionTimeInSeconds(maxExecutionTimeInSeconds); + l.setPropertyToDescribe(symmetric); + l.init(); + l.start(); + System.out.println(l.getCurrentlyBestEvaluatedAxioms(nrOfAxioms)); + } + +} Property changes on: trunk/components-core/src/test/java/org/dllearner/test/junit/PropertyLearningTest.java ___________________________________________________________________ Added: svn:mime-type + text/plain This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |