[Jsxe-cvs] SF.net SVN: jsxe: [1242] branches/jsxe2
Status: Inactive
Brought to you by:
ian_lewis
From: <ian...@us...> - 2006-09-06 23:22:20
|
Revision: 1242 http://svn.sourceforge.net/jsxe/?rev=1242&view=rev Author: ian_lewis Date: 2006-09-06 16:22:12 -0700 (Wed, 06 Sep 2006) Log Message: ----------- Made XMLDocument thread safe Modified Paths: -------------- branches/jsxe2/Changelog branches/jsxe2/src/net/sourceforge/jsxe/dom/XMLDocument.java branches/jsxe2/src/net/sourceforge/jsxe/util/ReadWriteLock.java Modified: branches/jsxe2/Changelog =================================================================== --- branches/jsxe2/Changelog 2006-09-06 19:34:32 UTC (rev 1241) +++ branches/jsxe2/Changelog 2006-09-06 23:22:12 UTC (rev 1242) @@ -1,3 +1,9 @@ +09/06/2006 Ian Lewis <Ian...@me...> + + * Added threading support to XMLDocument. Multi-threaded support is not + complete yet however. Currently jsXe only runs in the AWT thread. + * AdapterNode now uses the DOM3 renameNode() method to rename element nodes. + 08/31/2006 Ian Lewis <Ian...@me...> * Added new ContextSpecificActions to replace ViewSpecificActions. Now Modified: branches/jsxe2/src/net/sourceforge/jsxe/dom/XMLDocument.java =================================================================== --- branches/jsxe2/src/net/sourceforge/jsxe/dom/XMLDocument.java 2006-09-06 19:34:32 UTC (rev 1241) +++ branches/jsxe2/src/net/sourceforge/jsxe/dom/XMLDocument.java 2006-09-06 23:22:12 UTC (rev 1242) @@ -33,6 +33,7 @@ import net.sourceforge.jsxe.msg.UndoEvent; import net.sourceforge.jsxe.util.Log; import net.sourceforge.jsxe.util.MiscUtilities; +import net.sourceforge.jsxe.util.ReadWriteLock; import net.sourceforge.jsxe.dom.completion.*; import net.sourceforge.jsxe.dom.undo.*; import net.sourceforge.jsxe.dom.ls.*; @@ -340,54 +341,63 @@ * @return the old value of the property */ public String setProperty(String key, String value) { - String oldValue = getProperty(key); - if (oldValue == null || !oldValue.equals(value)) { - // do this first so NullPointerExceptions are thrown - oldValue = (String)props.setProperty(key, value); + synchronized(propertyLock) { + String oldValue = getProperty(key); - if (key.equals(ENCODING)) { - m_syncedWithContent = false; - } - if (key.equals(FORMAT_XML)) { - m_syncedWithContent = false; - if (Boolean.valueOf(value).booleanValue()) { - setProperty(WS_IN_ELEMENT_CONTENT, "false"); + if (oldValue == null || !oldValue.equals(value)) { + // do this first so NullPointerExceptions are thrown + oldValue = (String)props.setProperty(key, value); + + if (key.equals(ENCODING)) { + m_syncedWithContent = false; } - } - if (key.equals(WS_IN_ELEMENT_CONTENT)) { - m_syncedWithContent = false; - if (Boolean.valueOf(value).booleanValue()) { - setProperty(FORMAT_XML, "false"); + if (key.equals(FORMAT_XML)) { + m_syncedWithContent = false; + if (Boolean.valueOf(value).booleanValue()) { + setProperty(WS_IN_ELEMENT_CONTENT, "false"); + } } - } - if (key.equals(IS_USING_SOFT_TABS)) { - m_syncedWithContent = false; - } - if (key.equals(IS_VALIDATING)) { - //This is ugly. Need to rethink how this should happen. - if (Boolean.valueOf(value).booleanValue()) { - //syncContentWithDOM could change m_parsedMode - syncContentWithDOM(); - } else { - /* - If we are turning off validation then just clear the errors list. - No need to reparse or serialize. - */ - m_parseErrors = new ArrayList(); - m_parseFatalErrors = new ArrayList(); + if (key.equals(WS_IN_ELEMENT_CONTENT)) { + m_syncedWithContent = false; + if (Boolean.valueOf(value).booleanValue()) { + setProperty(FORMAT_XML, "false"); + } } - } - if (key.equals(INDENT)) { - if (MiscUtilities.isTrue(getProperty(FORMAT_XML)) - && MiscUtilities.isTrue(getProperty(IS_USING_SOFT_TABS))) - { + if (key.equals(IS_USING_SOFT_TABS)) { m_syncedWithContent = false; } + if (key.equals(IS_VALIDATING)) { + //This is ugly. Need to rethink how this should happen. + if (Boolean.valueOf(value).booleanValue()) { + //syncContentWithDOM could change m_parsedMode + try { + writeLock(); + + syncContentWithDOM(); + } finally { + writeUnlock(); + } + } else { + /* + If we are turning off validation then just clear the errors list. + No need to reparse or serialize. + */ + m_parseErrors = new ArrayList(); + m_parseFatalErrors = new ArrayList(); + } + } + if (key.equals(INDENT)) { + if (MiscUtilities.isTrue(getProperty(FORMAT_XML)) + && MiscUtilities.isTrue(getProperty(IS_USING_SOFT_TABS))) + { + m_syncedWithContent = false; + } + } + firePropertyChanged(key, oldValue); } - firePropertyChanged(key, oldValue); + return oldValue; } - return oldValue; }//}}} //{{{ getBooleanProperty() method @@ -461,14 +471,21 @@ */ public String getText(int start, int length) throws IOException { - if (start < 0 || length < 0 || start + length > m_content.getLength()) { - throw new ArrayIndexOutOfBoundsException(start + ":" + length); + try { + + writeLock(); + + if (start < 0 || length < 0 || start + length > m_content.getLength()) { + throw new ArrayIndexOutOfBoundsException(start + ":" + length); + } + + //if the document is well formed we go by the DOM + //if it's not we go by the source text. + syncContentWithDOM(); + return m_content.getText(start,length); + } finally { + writeUnlock(); } - - //if the document is well formed we go by the DOM - //if it's not we go by the source text. - syncContentWithDOM(); - return m_content.getText(start,length); }//}}} //{{{ getSegment() @@ -482,19 +499,25 @@ * @return the segment representing the text requested */ public Segment getSegment(int start, int length) throws IOException { - - if (start < 0 || length < 0 || start + length > m_content.getLength()) { - throw new ArrayIndexOutOfBoundsException(start + ":" + length); + try { + + writeLock(); + + if (start < 0 || length < 0 || start + length > m_content.getLength()) { + throw new ArrayIndexOutOfBoundsException(start + ":" + length); + } + + //if the document is well formed we go by the DOM + //if it's not we go by the source text. + if (m_parsedMode) { + syncContentWithDOM(); + } + Segment seg = new Segment(); + m_content.getText(start, length, seg); + return seg; + } finally { + writeUnlock(); } - - //if the document is well formed we go by the DOM - //if it's not we go by the source text. - if (m_parsedMode) { - syncContentWithDOM(); - } - Segment seg = new Segment(); - m_content.getText(start, length, seg); - return seg; }//}}} //{{{ getLength() @@ -503,10 +526,17 @@ * @return the length of the document */ public int getLength() { - - syncContentWithDOM(); - - return m_content.getLength(); + try { + writeLock(); + + syncContentWithDOM(); + + // no need to lock since this just returns a value and that's it + return m_content.getLength(); + } finally { + Log.log(Log.DEBUG, this, "writeUnlock()"); + writeUnlock(); + } }//}}} //{{{ insertText() @@ -517,16 +547,27 @@ * @throws IOException if the text could not be inserted */ public void insertText(int offset, String text) throws IOException { - if (text.length() > 0) { - Log.log(Log.DEBUG, this, "insertText: "+offset+": "+text); - syncContentWithDOM(); - m_content.insert(offset, text); - m_parsedMode = false; - m_adapterNode = null; - if (!getFlag(UNDO_IN_PROGRESS)) { - addUndoableEdit(new InsertEdit(this, offset, text)); + + try { + writeLock(); + + if (offset < 0 || offset > m_content.getLength()) { + throw new ArrayIndexOutOfBoundsException(offset); } - fireStructureChanged(null); + + if (text.length() > 0) { + // Log.log(Log.DEBUG, this, "insertText: "+offset+": "+text); + syncContentWithDOM(); + m_content.insert(offset, text); + m_parsedMode = false; + m_adapterNode = null; + if (!getFlag(UNDO_IN_PROGRESS)) { + addUndoableEdit(new InsertEdit(this, offset, text)); + } + fireStructureChanged(null); + } + } finally { + writeUnlock(); } }//}}} @@ -538,21 +579,35 @@ * @throws IOException if the text could not be removed */ public void removeText(int offset, int length) throws IOException { - if (length > 0) { - String text = getText(offset,length); - Log.log(Log.DEBUG, this, "removeText: "+offset+": "+text); - m_content.remove(offset, length); - m_parsedMode = false; - m_adapterNode = null; - if (!getFlag(UNDO_IN_PROGRESS)) { - addUndoableEdit(new RemoveEdit(this, offset, text)); + + try { + + writeLock(); + + if (offset < 0 || length < 0 || offset + length > m_content.getLength()) { + throw new ArrayIndexOutOfBoundsException(offset + ":" + length); } - fireStructureChanged(null); + + if (length > 0) { + String text = getText(offset,length); + Log.log(Log.DEBUG, this, "removeText: "+offset+": "+text); + m_content.remove(offset, length); + m_parsedMode = false; + m_adapterNode = null; + if (!getFlag(UNDO_IN_PROGRESS)) { + addUndoableEdit(new RemoveEdit(this, offset, text)); + } + fireStructureChanged(null); + } + } finally { + writeUnlock(); } }//}}} //}}} + //{{{ XML Document Methods + //{{{ checkWellFormedness() /** * Checks the wellformedness of the document and throws appropriate @@ -564,14 +619,18 @@ * @throws IOException if there was a problem reading the document */ public boolean checkWellFormedness() throws SAXParseException, SAXException, ParserConfigurationException, IOException { - if (!m_parsedMode) { - parseDocument(); - m_adapterNode = new AdapterNode(this, m_document); - // m_adapterNode.addAdapterNodeListener(docAdapterListener); - // m_syncedWithContent = true; - m_parsedMode=true; + try { + readLock(); + + if (!m_parsedMode) { + parseDocument(); + m_adapterNode = new AdapterNode(this, m_document); + m_parsedMode=true; + } + return m_parsedMode; + } finally { + readUnlock(); } - return m_parsedMode; }//}}} //{{{ getDocType() @@ -700,9 +759,14 @@ * @since jsXe 0.4 pre3 */ public List getErrors() { - syncContentWithDOM(); - parseWithoutUpdate(); - return m_parseErrors; + try { + writeLock(); + syncContentWithDOM(); + parseWithoutUpdate(); + return m_parseErrors; + } finally { + writeUnlock(); + } }//}}} //{{{ isWellFormed() @@ -735,16 +799,21 @@ */ public boolean isValid() throws IOException { if (Boolean.valueOf(getProperty(IS_VALIDATING)).booleanValue()) { - /* - This needs to be done every time we check for validity even - if this document hasn't changed since the DTD or Schema document - we are validating against may have changed in the meantime. - */ - - syncContentWithDOM(); - parseWithoutUpdate(); - - return (m_parseErrors.size() == 0 && m_parseFatalErrors.size() == 0); + try { + writeLock(); + /* + This needs to be done every time we check for validity even + if this document hasn't changed since the DTD or Schema document + we are validating against may have changed in the meantime. + */ + + syncContentWithDOM(); + parseWithoutUpdate(); + + return (m_parseErrors.size() == 0 && m_parseFatalErrors.size() == 0); + } finally { + writeUnlock(); + } } else { return false; } @@ -813,6 +882,20 @@ return getNoNamespaceCompletionInfo().getEntities(); }//}}} + //{{{ setEntityResolver() + /** + * Sets the EntityResolver object that is used when resolving external + * entities. + * @param resolver the entity resolver + */ + public void setEntityResolver(EntityResolver resolver) { + m_entityResolver = resolver; + }//}}} + + //}}} + + //{{{ I/O Methods + //{{{ serialize() /** * Writes the XML document to the output stream specified. @@ -823,79 +906,86 @@ */ public void serialize(OutputStream out) throws IOException, UnsupportedEncodingException { - boolean parsedBeforeSerialization = m_parsedMode; - - String newLine = getProperty(LINE_SEPARATOR); - - syncContentWithDOM(); - - String encoding = getProperty(ENCODING); - if (encoding.equals(MiscUtilities.UTF_8_Y)) { - // not supported by Java... - out.write(UTF8_MAGIC_1); - out.write(UTF8_MAGIC_2); - out.write(UTF8_MAGIC_3); - out.flush(); - encoding = "UTF-8"; - } - - //now just write out the text. - int length = m_content.getLength(); - int index = 0; - BufferedWriter outbuf = new BufferedWriter(new OutputStreamWriter(out, encoding), IO_BUFFER_SIZE); - Segment seg = new Segment(); - - while (index < length) { - int size = WRITE_SIZE; - try { - size = Math.min(length - index, WRITE_SIZE); - } catch(NumberFormatException nf) { - Log.log(Log.ERROR, this, nf); + try { + + writeLock(); + + boolean parsedBeforeSerialization = m_parsedMode; + + String newLine = getProperty(LINE_SEPARATOR); + + syncContentWithDOM(); + + String encoding = getProperty(ENCODING); + if (encoding.equals(MiscUtilities.UTF_8_Y)) { + // not supported by Java... + out.write(UTF8_MAGIC_1); + out.write(UTF8_MAGIC_2); + out.write(UTF8_MAGIC_3); + out.flush(); + encoding = "UTF-8"; } - // out.write(m_content.getText(index, size).getBytes(getProperty(ENCODING)), index, size); - m_content.getText(index, size, seg); + //now just write out the text. + int length = m_content.getLength(); + int index = 0; + BufferedWriter outbuf = new BufferedWriter(new OutputStreamWriter(out, encoding), IO_BUFFER_SIZE); + Segment seg = new Segment(); - int startOffset = seg.offset; - int endOffset = size + seg.offset; - - for (int i=startOffset; i<endOffset; i++) { - if (seg.array[i]=='\n') { - outbuf.write(seg.array, seg.offset, i - seg.offset); - outbuf.write(newLine.toCharArray(), 0, newLine.length()); - - //add 1 because of \n character, - seg.count -= i-seg.offset+1; - seg.offset += i-seg.offset+1; + while (index < length) { + int size = WRITE_SIZE; + try { + size = Math.min(length - index, WRITE_SIZE); + } catch(NumberFormatException nf) { + Log.log(Log.ERROR, this, nf); } + + // out.write(m_content.getText(index, size).getBytes(getProperty(ENCODING)), index, size); + m_content.getText(index, size, seg); + + int startOffset = seg.offset; + int endOffset = size + seg.offset; + + for (int i=startOffset; i<endOffset; i++) { + if (seg.array[i]=='\n') { + outbuf.write(seg.array, seg.offset, i - seg.offset); + outbuf.write(newLine.toCharArray(), 0, newLine.length()); + + //add 1 because of \n character, + seg.count -= i-seg.offset+1; + seg.offset += i-seg.offset+1; + } + } + + //write the rest + outbuf.write(seg.array, seg.offset, seg.count); + index += size; } - //write the rest - outbuf.write(seg.array, seg.offset, seg.count); - index += size; - } - - outbuf.close(); - - //if something changed in the structure while serializing - //basically we don't want serialize() to cause the XMLDocument - //to go from parsed mode to non-parsed mode - if (!m_parsedMode && parsedBeforeSerialization) { - try { - checkWellFormedness(); - } catch (SAXException saxe) { - throw new IOException(saxe.getMessage()); - } catch (ParserConfigurationException pce) { - throw new IOException(pce.getMessage()); - } finally { - /* - if there is an error parsing we want to be in parsed mode - using the old DOM before serializing. - if there is no error, we want to be in parsed mode with the - new DOM. - */ - m_parsedMode = true; + outbuf.close(); + + //if something changed in the structure while serializing + //basically we don't want serialize() to cause the XMLDocument + //to go from parsed mode to non-parsed mode + if (!m_parsedMode && parsedBeforeSerialization) { + try { + checkWellFormedness(); + } catch (SAXException saxe) { + throw new IOException(saxe.getMessage()); + } catch (ParserConfigurationException pce) { + throw new IOException(pce.getMessage()); + } finally { + /* + if there is an error parsing we want to be in parsed mode + using the old DOM before serializing. + if there is no error, we want to be in parsed mode with the + new DOM. + */ + m_parsedMode = true; + } } + } finally { + writeUnlock(); } }//}}} @@ -916,36 +1006,6 @@ return value; }//}}} - //{{{ setEntityResolver() - /** - * Sets the EntityResolver object that is used when resolving external - * entities. - * @param resolver the entity resolver - */ - public void setEntityResolver(EntityResolver resolver) { - m_entityResolver = resolver; - }//}}} - - //{{{ setURI() - /** - * Sets the URI for the location of this document. - * @param uri the uri specifying the location of this document. Can be null. - * @since jsXe 0.4 pre4 - */ - public void setURI(URI uri) { - m_uri = uri; - }//}}} - - //{{{ getURI() - /** - * Gets the URI for the location of this document. - * @return the uri specifying the location of this document. Can be null. - * @since jsXe 0.4 pre4 - */ - public URI getURI() { - return m_uri; - }//}}} - //{{{ setModel() /** * Sets up the XMLDocument given a Reader. The existing content is @@ -1115,6 +1175,68 @@ // } }//}}} + //}}} + + //{{{ Thread safety + + //{{{ readLock() method + /** + * The buffer is guaranteed not to change between calls to + * {@link #readLock()} and {@link #readUnlock()}. + */ + public void readLock() { + lock.readLock(); + } //}}} + + //{{{ readUnlock() method + /** + * The buffer is guaranteed not to change between calls to + * {@link #readLock()} and {@link #readUnlock()}. + */ + public void readUnlock() { + lock.readUnlock(); + } //}}} + + //{{{ writeLock() method + /** + * Attempting to obtain read lock will block between calls to + * {@link #writeLock()} and {@link #writeUnlock()}. + */ + public void writeLock() { + lock.writeLock(); + } //}}} + + //{{{ writeUnlock() method + /** + * Attempting to obtain read lock will block between calls to + * {@link #writeLock()} and {@link #writeUnlock()}. + */ + public void writeUnlock() { + lock.writeUnlock(); + } //}}} + + //}}} + + //{{{ setURI() + /** + * Sets the URI for the location of this document. + * @param uri the uri specifying the location of this document. Can be null. + * @since jsXe 0.4 pre4 + */ + public void setURI(URI uri) { + m_uri = uri; + }//}}} + + //{{{ getURI() + /** + * Gets the URI for the location of this document. + * @return the uri specifying the location of this document. Can be null. + * @since jsXe 0.4 pre4 + */ + public URI getURI() { + return m_uri; + }//}}} + //{{{ addXMLDocumentListener() /** * Registers a change listener with the XMLDocument @@ -1222,11 +1344,13 @@ //{{{ syncContentWithDOM() /** * Write the DOM to the content manager given the current serialization and - * formatting options. + * formatting options. This method is not thread safe. You should call + * writeLock() before and writeUnlock() after calling this method. */ private void syncContentWithDOM() { if (m_parsedMode) { if (!m_syncedWithContent) { + try { Log.log(Log.MESSAGE, this, "Serializing document"); //since we are in parsed mode let's serialize to the content @@ -1256,6 +1380,9 @@ m_parsedMode = false; try { + + readLock(); + parseDocument(); //Why was this set to false? why would we want to //serialize the document again since nothing's changed? @@ -1267,6 +1394,8 @@ } catch (Exception e) { //If an error occurs then we're in trouble jsXe.exiterror(this, e, 1); + } finally { + readUnlock(); } fireStructureChanged(null); } @@ -1302,7 +1431,8 @@ //{{{ parseDocument() /** * Parses the document with the current options. After this is called - * m_adapterNode and m_parsedMode must be updated. + * m_adapterNode and m_parsedMode must be updated. The document text content + * should be up to date compared to the document structure. * @since jsXe 0.4 pre1 */ public void parseDocument() throws SAXParseException, SAXException, ParserConfigurationException, IOException { @@ -1330,9 +1460,7 @@ builder.setEntityResolver(m_entityResolver); } - //Temporary fix to allow parsing of documnts with multi-byte characters - // Document doc = builder.parse(new ContentManagerInputStream(m_content)); - String text = getText(0, getLength()); + String text = m_content.getText(0,m_content.getLength()); Document doc = builder.parse(new InputSource(new StringReader(text))); doc.getDocumentElement().normalize(); //}}} @@ -1352,7 +1480,8 @@ //{{{ parseWithoutUpdate() /** * Parses the document without updating the DOM. This method does, however, - * update completion info and parse errors. + * update completion info and parse errors. The document text content should + * be up to date compared to the document structure. */ public void parseWithoutUpdate() { Log.log(Log.MESSAGE, this, (m_uri != null ? "Validating Document: "+m_uri.toString() : "Validating Document")); @@ -1387,7 +1516,7 @@ try { //Temporary fix to allow parsing of documnts with multi-byte characters // reader.parse(new InputSource(new ContentManagerInputStream(m_content))); - String text = getText(0, getLength()); + String text = m_content.getText(0,m_content.getLength()); reader.parse(new InputSource(new StringReader(text))); } catch(SAXException se) { //validation errors @@ -2092,6 +2221,12 @@ private boolean m_addedToCompoundEdits = false; private int m_compoundEditCount = 0; + /** + * I/O lock. + */ + private ReadWriteLock lock = new ReadWriteLock(); + private Object propertyLock = new Object(); + // private XMLDocAdapterListener docAdapterListener = new XMLDocAdapterListener(); //}}} Modified: branches/jsxe2/src/net/sourceforge/jsxe/util/ReadWriteLock.java =================================================================== --- branches/jsxe2/src/net/sourceforge/jsxe/util/ReadWriteLock.java 2006-09-06 19:34:32 UTC (rev 1241) +++ branches/jsxe2/src/net/sourceforge/jsxe/util/ReadWriteLock.java 2006-09-06 23:22:12 UTC (rev 1242) @@ -56,28 +56,22 @@ * write unlock * write unlock */ -public class ReadWriteLock -{ +public class ReadWriteLock { + //{{{ readLock() method - public synchronized void readLock() - { + public synchronized void readLock() { // this seems to make nested readLock() calls work okay. // but I have no idea if it actually fixes things or not. - if (activeReaders != 0 || allowRead()) - { - ++activeReaders; - //readers.addElement(Thread.currentThread()); - return; + if (activeReaders != 0 || allowRead()) { + // ++activeReaders; + // //readers.addElement(Thread.currentThread()); + // return; } ++waitingReaders; - while (!allowRead()) - { - try - { + while (!allowRead()) { + try { wait(); - } - catch (InterruptedException e) - { + } catch (InterruptedException e) { --waitingReaders; // Roll back state. Log.log(Log.ERROR,this,e); return; @@ -89,10 +83,11 @@ } //}}} //{{{ readUnlock() method - public synchronized void readUnlock() - { - if(activeReaders == 0) + public synchronized void readUnlock() { + + if (activeReaders == 0) { throw new InternalError("Unbalanced readLock()/readUnlock() calls"); + } --activeReaders; //readers.removeElement(Thread.currentThread()); @@ -100,33 +95,27 @@ } //}}} //{{{ writeLock() method - public synchronized void writeLock() - { - if (writerThread != null) - { + public synchronized void writeLock() { + + if (writerThread != null) { // Write in progress. - if (Thread.currentThread() == writerThread) - { - // Same thread. - ++lockCount; - return; - } + // if (Thread.currentThread() == writerThread) + // { + // // Same thread. + // ++lockCount; + // return; + // } } - if (allowWrite()) - { + if (allowWrite()) { claimWriteLock(); return; } ++waitingWriters; - while (!allowWrite()) - { - try - { + while (!allowWrite()) { + try { wait(); - } - catch (InterruptedException e) - { + } catch (InterruptedException e) { --waitingWriters; Log.log(Log.ERROR,this,e); return; @@ -137,16 +126,17 @@ } //}}} //{{{ writeUnlock() method - public synchronized void writeUnlock() - { - if(activeWriters != 1 || lockCount <= 0) + public synchronized void writeUnlock() { + + if (activeWriters != 1 || lockCount <= 0) { throw new InternalError("Unbalanced writeLock()/writeUnlock() calls"); + } - if(Thread.currentThread() != writerThread) + if (Thread.currentThread() != writerThread) { throw new InternalError("writeUnlock() from wrong thread"); + } - if (--lockCount == 0) - { + if (--lockCount == 0) { --activeWriters; writerThread = null; notifyAll(); This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |