Eclipse Community Forums
Forum Search:

Search      Help    Register    Login    Home
Home » Modeling » EMF » Strange behavior with substitution groups
Strange behavior with substitution groups [message #1077072] Thu, 01 August 2013 08:00 Go to next message
Heribert  Schütz is currently offline Heribert Schütz
Messages: 24
Registered: July 2009
Junior Member
Hi,

I am working with XML files whose format I do not control (but I can modify my copy of the schema as long as it remains compatible).

Here's a simplified example:

<?xml version="1.0" encoding="UTF-8"?>
<list>
  <itemKindA name="some item of kind A" />
  <itemKindB name="some item of kind B" />
  <itemKindA name="another item of kind A" />
</list>


The original XML schema uses a choice (with maxOccurs="unbounded") between elements "itemKindA" and "itemKindB". This was translated into a feature map, which didn't work for me because I also want to use the Ecore model for Xtext, which does not work with feature maps.

In some older forum thread I found a link http://ed-merks.blogspot.de/2007/12/winters-icy-grip.html providing a solution for me: substitution groups. So here is my tweaked schema (again largely simplified):

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore"
    ecore:package="test.substgroup.example"
    ecore:name="example"
    ecore:ignoreSubstitutionGroups="true"
  >

  <xs:element name="list" type="list_t" />
  <xs:complexType name="list_t">
    <xs:sequence>
      <xs:element ref="item" minOccurs="0" maxOccurs="unbounded" />
    </xs:sequence>
  </xs:complexType>

  <xs:element name="item" ecore:name="items" type="item_t" abstract="true" />
  <xs:complexType name="item_t" abstract="true">
  	<xs:sequence />
    <xs:attribute name="name" type="xs:string" />
  </xs:complexType>

  <xs:element name="itemKindA" type="itemKindA_t" substitutionGroup="item" />
  <xs:complexType name="itemKindA_t">
    <xs:complexContent>
      <xs:extension base="item_t">
        <xs:sequence />
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>

  <xs:element name="itemKindB" type="itemKindB_t" substitutionGroup="item" />
  <xs:complexType name="itemKindB_t">
    <xs:complexContent>
      <xs:extension base="item_t">
        <xs:sequence />
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>
</xs:schema>


The generated Ecore model is as I need it. Here is some example Xtend code using it:

package test.substgroup

import java.io.ByteArrayOutputStream
import org.eclipse.emf.common.util.URI
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.emf.ecore.resource.ResourceSet
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl
import org.eclipse.emf.ecore.xmi.XMLResource
import org.eclipse.emf.ecore.xmi.impl.ElementHandlerImpl
import test.substgroup.example.ExampleFactory
import test.substgroup.example.ExamplePackage
import test.substgroup.example.ListT
import test.substgroup.example.util.ExampleResourceFactoryImpl

class Main {
    static boolean doMagicInitialization = false;

    def static void main(String[] args) {
        val resourceSet = new ResourceSetImpl as ResourceSet => [
            resourceFactoryRegistry.extensionToFactoryMap
                .put(Resource::Factory::Registry::DEFAULT_EXTENSION, new MyResourceFactoryImpl);
            packageRegistry.put(ExamplePackage::eNS_URI, ExamplePackage.eINSTANCE);

            if (doMagicInitialization)
                createResource(URI::createFileURI("xxx")) => [
                    contents += ExampleFactory::eINSTANCE.createListT;
                    save(new ByteArrayOutputStream, null);
                ]
        ]

        val resource = resourceSet.getResource(URI::createFileURI("data/data.xml"), true);
 
        val list = resource.contents.get(0) as ListT;
        for (x: list.items)
            println(x);
    }
}

class MyResourceFactoryImpl extends ExampleResourceFactoryImpl {
    override createResource(URI uri) {
        super.createResource(uri) as XMLResource => [
            defaultSaveOptions.put(XMLResource.OPTION_ELEMENT_HANDLER, new ElementHandlerImpl(true));
            defaultLoadOptions.put(XMLResource.OPTION_SUPPRESS_DOCUMENT_ROOT, true);
        ]
    }
}


If we ignore the "magic initialization" section for now, this code should simply read an XML file like the one given above. But it fails with an exception saying that the feature "itemKindA" was not found.

Strangely enough, some other test cases did work. After a lot of experimentation I found that loading resources worked if some resource was written before. So that's what the "magic initialization" above does. If I switch it on, the example works.

So now my question is: Should I run some (not so hacky) initialization code before loading XML resources?

Did I use EMF wrongly or is this an EMF bug?

Regards,
Heribert.
Re: Strange behavior with substitution groups [message #1077120 is a reply to message #1077072] Thu, 01 August 2013 09:11 Go to previous messageGo to next message
Ed Merks is currently offline Ed Merks
Messages: 26135
Registered: July 2009
Senior Member
Heribert,

Comments below.

On 01/08/2013 10:00 AM, Heribert Schütz wrote:
> Hi,
>
> I am working with XML files whose format I do not control (but I can
> modify my copy of the schema as long as it remains compatible).
>
> Here's a simplified example:
>
>
> <?xml version="1.0" encoding="UTF-8"?>
> <list>
> <itemKindA name="some item of kind A" />
> <itemKindB name="some item of kind B" />
> <itemKindA name="another item of kind A" />
> </list>
>
>
> The original XML schema uses a choice (with maxOccurs="unbounded")
> between elements "itemKindA" and "itemKindB". This was translated
> into a feature map, which didn't work for me because I also want to
> use the Ecore model for Xtext, which does not work with feature maps.
I see.
>
> In some older forum thread I found a link
> http://ed-merks.blogspot.de/2007/12/winters-icy-grip.html providing a
> solution for me: substitution groups. So here is my tweaked schema
> (again largely simplified):
>
>
> <?xml version="1.0" encoding="UTF-8"?>
> <xs:schema
> xmlns:xs="http://www.w3.org/2001/XMLSchema"
> xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore"
> ecore:package="test.substgroup.example"
> ecore:name="example"
> ecore:ignoreSubstitutionGroups="true"
> >
>
> <xs:element name="list" type="list_t" />
> <xs:complexType name="list_t">
> <xs:sequence>
> <xs:element ref="item" minOccurs="0" maxOccurs="unbounded" />
> </xs:sequence>
> </xs:complexType>
>
> <xs:element name="item" ecore:name="items" type="item_t"
> abstract="true" />
> <xs:complexType name="item_t" abstract="true">
> <xs:sequence />
> <xs:attribute name="name" type="xs:string" />
> </xs:complexType>
>
> <xs:element name="itemKindA" type="itemKindA_t"
> substitutionGroup="item" />
> <xs:complexType name="itemKindA_t">
> <xs:complexContent>
> <xs:extension base="item_t">
> <xs:sequence />
> </xs:extension>
> </xs:complexContent>
> </xs:complexType>
>
> <xs:element name="itemKindB" type="itemKindB_t"
> substitutionGroup="item" />
> <xs:complexType name="itemKindB_t">
> <xs:complexContent>
> <xs:extension base="item_t">
> <xs:sequence />
> </xs:extension>
> </xs:complexContent>
> </xs:complexType>
> </xs:schema>
>
>
> The generated Ecore model is as I need it. Here is some example Xtend
> code using it:
>
>
> package test.substgroup
>
> import java.io.ByteArrayOutputStream
> import org.eclipse.emf.common.util.URI
> import org.eclipse.emf.ecore.resource.Resource
> import org.eclipse.emf.ecore.resource.ResourceSet
> import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl
> import org.eclipse.emf.ecore.xmi.XMLResource
> import org.eclipse.emf.ecore.xmi.impl.ElementHandlerImpl
> import test.substgroup.example.ExampleFactory
> import test.substgroup.example.ExamplePackage
> import test.substgroup.example.ListT
> import test.substgroup.example.util.ExampleResourceFactoryImpl
>
> class Main {
> static boolean doMagicInitialization = false;
>
> def static void main(String[] args) {
> val resourceSet = new ResourceSetImpl as ResourceSet => [
> resourceFactoryRegistry.extensionToFactoryMap
> .put(Resource::Factory::Registry::DEFAULT_EXTENSION, new
> MyResourceFactoryImpl);
> packageRegistry.put(ExamplePackage::eNS_URI,
> ExamplePackage.eINSTANCE);
>
> if (doMagicInitialization)
> createResource(URI::createFileURI("xxx")) => [
What does createResource do?
> contents += ExampleFactory::eINSTANCE.createListT;
> save(new ByteArrayOutputStream, null);
> ]
> ]
>
> val resource =
> resourceSet.getResource(URI::createFileURI("data/data.xml"), true);
>
> val list = resource.contents.get(0) as ListT;
> for (x: list.items)
> println(x);
> }
> }
>
> class MyResourceFactoryImpl extends ExampleResourceFactoryImpl {
> override createResource(URI uri) {
> super.createResource(uri) as XMLResource => [
> defaultSaveOptions.put(XMLResource.OPTION_ELEMENT_HANDLER, new
> ElementHandlerImpl(true));
> defaultLoadOptions.put(XMLResource.OPTION_SUPPRESS_DOCUMENT_ROOT, true);
> ]
> }
> }
>
>
> If we ignore the "magic initialization" section for now, this code
> should simply read an XML file like the one given above. But it fails
> with an exception saying that the feature "itemKindA" was not found.
That sounds like what would happen if your MyResourceFactoryImpl wasn't
actually used to create the resource. You can set a breakpoint to
confirm that...
>
> Strangely enough, some other test cases did work. After a lot of
> experimentation I found that loading resources worked if some resource
> was written before. So that's what the "magic initialization" above
> does. If I switch it on, the example works.
I guess I can't see what all the magic is doing...
>
> So now my question is: Should I run some (not so hacky)
> initialization code before loading XML resources?
No, best we figure out what's wrong.
>
> Did I use EMF wrongly or is this an EMF bug?
Does the generated ExampleExample.java in the generated *.tests project
if you invoke Generate Test Code work correctly?
>
> Regards,
> Heribert.
>
Re: Strange behavior with substitution groups [message #1077167 is a reply to message #1077120] Thu, 01 August 2013 10:27 Go to previous messageGo to next message
Heribert  Schütz is currently offline Heribert Schütz
Messages: 24
Registered: July 2009
Junior Member
Hi Ed,

thanks for your quick reply.

Running ExampleExample.java from the generated test code on my XML example file from the initial post throws the same FeatureNotFoundException that I get with my test code (without the "magic initialization"):

Problem loading file:/home/hs/workspace/test.substgroup.tests/../test.substgroup/data/data.xml
org.eclipse.emf.ecore.resource.impl.ResourceSetImpl$1DiagnosticWrappedException: org.eclipse.emf.ecore.xmi.FeatureNotFoundException: Feature 'itemKindA' not found. (file:/home/hs/workspace/test.substgroup.tests/../test.substgroup/data/data.xml, 3, 43)
	at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.handleDemandLoadException(ResourceSetImpl.java:319)
	at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.demandLoadHelper(ResourceSetImpl.java:278)
	at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.getResource(ResourceSetImpl.java:406)
	at test.substgroup.example.tests.ExampleExample.main(ExampleExample.java:88)
Caused by: org.eclipse.emf.ecore.xmi.FeatureNotFoundException: Feature 'itemKindA' not found. (file:/home/hs/workspace/test.substgroup.tests/../test.substgroup/data/data.xml, 3, 43)
	at org.eclipse.emf.ecore.xmi.impl.XMLHandler.reportUnknownFeature(XMLHandler.java:1998)
	at org.eclipse.emf.ecore.xmi.impl.XMLHandler.handleUnknownFeature(XMLHandler.java:1962)
	at org.eclipse.emf.ecore.xmi.impl.XMLHandler.handleFeature(XMLHandler.java:1906)
	at org.eclipse.emf.ecore.xmi.impl.XMLHandler.processElement(XMLHandler.java:1030)
	at org.eclipse.emf.ecore.xmi.impl.XMLHandler.startElement(XMLHandler.java:1008)
	at org.eclipse.emf.ecore.xmi.impl.XMLHandler.startElement(XMLHandler.java:719)
	at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.startElement(AbstractSAXParser.java:506)
	at com.sun.org.apache.xerces.internal.parsers.AbstractXMLDocumentParser.emptyElement(AbstractXMLDocumentParser.java:182)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanStartElement(XMLDocumentFragmentScannerImpl.java:1303)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2717)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:607)
	at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:489)
	at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:835)
	at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:764)
	at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:123)
	at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1210)
	at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:568)
	at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl.parse(SAXParserImpl.java:302)
	at org.eclipse.emf.ecore.xmi.impl.XMLLoadImpl.load(XMLLoadImpl.java:175)
	at org.eclipse.emf.ecore.xmi.impl.XMLResourceImpl.doLoad(XMLResourceImpl.java:253)
	at org.eclipse.emf.ecore.resource.impl.ResourceImpl.load(ResourceImpl.java:1518)
	at org.eclipse.emf.ecore.resource.impl.ResourceImpl.load(ResourceImpl.java:1297)
	at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.demandLoad(ResourceSetImpl.java:259)
	at org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.demandLoadHelper(ResourceSetImpl.java:274)
	... 2 more


According to your suggestion I put a breakpoint in MyResourceFactoryImpl.createResource(...), actually in the line starting with "defaultSaveOptions". Depending on how I invoked my test code it was reached once or twice:

  • without "magic initialization" the breakpoint was reached once, from within the call to resourceSet.getResource(...).
  • with "magic initialization" the breakpoint was reached twice: From within my call to createResource(URI::createFileURI("xxx")) in the magic code, and a second time as above from resourceSet.getResource(...).

So MyResourceFactoryImpl is being used as expected.

Regarding your question what createResource in the magic code does: It is simply a shortcut for "it.createResource(...)" and "it" is a ResourceSetImpl.

Or did you ask what the entire "magic initialization" does? It uses the resource set to create a resource (with some irrelevant URI). Then it adds a single object to the resource content and finally it writes the resource to some stream (which will be ignored).

Any more ideas?

Regards,
Heribert.
Re: Strange behavior with substitution groups [message #1077201 is a reply to message #1077167] Thu, 01 August 2013 11:08 Go to previous messageGo to next message
Ed Merks is currently offline Ed Merks
Messages: 26135
Registered: July 2009
Senior Member
Heribert,

Please export the model project and test project to a zip, open a
bugzilla and attach it. Then I can try to reproduce the problem
locally. That's likely faster than trying to guess at the problem from
a description of the symptoms.


On 01/08/2013 12:27 PM, Heribert Schütz wrote:
> Hi Ed,
>
> thanks for your quick reply.
>
> Running ExampleExample.java from the generated test code on my XML
> example file from the initial post throws the same
> FeatureNotFoundException that I get with my test code (without the
> "magic initialization"):
>
>
> Problem loading
> file:/home/hs/workspace/test.substgroup.tests/../test.substgroup/data/data.xml
> org.eclipse.emf.ecore.resource.impl.ResourceSetImpl$1DiagnosticWrappedException:
> org.eclipse.emf.ecore.xmi.FeatureNotFoundException: Feature
> 'itemKindA' not found.
> (file:/home/hs/workspace/test.substgroup.tests/../test.substgroup/data/data.xml,
> 3, 43)
> at
> org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.handleDemandLoadException(ResourceSetImpl.java:319)
> at
> org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.demandLoadHelper(ResourceSetImpl.java:278)
> at
> org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.getResource(ResourceSetImpl.java:406)
> at
> test.substgroup.example.tests.ExampleExample.main(ExampleExample.java:88)
> Caused by: org.eclipse.emf.ecore.xmi.FeatureNotFoundException: Feature
> 'itemKindA' not found.
> (file:/home/hs/workspace/test.substgroup.tests/../test.substgroup/data/data.xml,
> 3, 43)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLHandler.reportUnknownFeature(XMLHandler.java:1998)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLHandler.handleUnknownFeature(XMLHandler.java:1962)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLHandler.handleFeature(XMLHandler.java:1906)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLHandler.processElement(XMLHandler.java:1030)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLHandler.startElement(XMLHandler.java:1008)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLHandler.startElement(XMLHandler.java:719)
> at
> com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.startElement(AbstractSAXParser.java:506)
> at
> com.sun.org.apache.xerces.internal.parsers.AbstractXMLDocumentParser.emptyElement(AbstractXMLDocumentParser.java:182)
> at
> com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanStartElement(XMLDocumentFragmentScannerImpl.java:1303)
> at
> com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2717)
> at
> com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:607)
> at
> com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:489)
> at
> com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:835)
> at
> com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:764)
> at
> com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:123)
> at
> com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1210)
> at
> com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:568)
> at
> com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl.parse(SAXParserImpl.java:302)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLLoadImpl.load(XMLLoadImpl.java:175)
> at
> org.eclipse.emf.ecore.xmi.impl.XMLResourceImpl.doLoad(XMLResourceImpl.java:253)
> at
> org.eclipse.emf.ecore.resource.impl.ResourceImpl.load(ResourceImpl.java:1518)
> at
> org.eclipse.emf.ecore.resource.impl.ResourceImpl.load(ResourceImpl.java:1297)
> at
> org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.demandLoad(ResourceSetImpl.java:259)
> at
> org.eclipse.emf.ecore.resource.impl.ResourceSetImpl.demandLoadHelper(ResourceSetImpl.java:274)
> ... 2 more
>
>
> According to your suggestion I put a breakpoint in
> MyResourceFactoryImpl.createResource(...), actually in the line
> starting with "defaultSaveOptions". Depending on how I invoked my
> test code it was reached once or twice:
>
> without "magic initialization" the breakpoint was reached once, from
> within the call to resourceSet.getResource(...).
> with "magic initialization" the breakpoint was reached twice: From
> within my call to createResource(URI::createFileURI("xxx")) in the
> magic code, and a second time as above from resourceSet.getResource(...).
>
> So MyResourceFactoryImpl is being used as expected.
>
> Regarding your question what createResource in the magic code does:
> It is simply a shortcut for "it.createResource(...)" and "it" is a
> ResourceSetImpl.
>
> Or did you ask what the entire "magic initialization" does? It uses
> the resource set to create a resource (with some irrelevant URI).
> Then it adds a single object to the resource content and finally it
> writes the resource to some stream (which will be ignored).
>
> Any more ideas?
>
> Regards,
> Heribert.
>
Re: Strange behavior with substitution groups [message #1077214 is a reply to message #1077201] Thu, 01 August 2013 11:34 Go to previous messageGo to next message
Heribert  Schütz is currently offline Heribert Schütz
Messages: 24
Registered: July 2009
Junior Member
Hi Ed,

find the zip archive attached. The projects consist essentially of the three files from the initial post. Everything else is generated.

Heribert.
Re: Strange behavior with substitution groups [message #1077290 is a reply to message #1077214] Thu, 01 August 2013 13:47 Go to previous messageGo to next message
Ed Merks is currently offline Ed Merks
Messages: 26135
Registered: July 2009
Senior Member
Heribert,

Okay, I can reproduce the strange behavior. It's related to caching of
the extend meta data lookup and is specific to the special treatment
needed for null namespace schemas. If I modify your generated factory
resource factory like this, the problem goes away.

public ExampleResourceFactoryImpl() {
super();
extendedMetaData = new BasicExtendedMetaData(new
EPackageRegistryImpl(EPackage.Registry.INSTANCE)) {
{extendedMetaDataHolderCache = new HashMap<EModelElement, Object>();}};
extendedMetaData.putPackage(null, ExamplePackage.eINSTANCE);
}

Note the initialization of a map used for caching (and keep in mind that
even this specific creation of a specialized extended meta data instance
is also special treatment for null namespace schemas) . You could put
code like this in the constructor of your derived factory if you don't
want to modify generated code.

I'm pretty sure the problem won't be present if you weren't using a
no-namespace schema...

Please open a bugzilla with your test case so I can look into a better
fix for the problem.


On 01/08/2013 1:34 PM, Heribert Schütz wrote:
> Hi Ed,
>
> find the zip archive attached. The projects consist essentially of the three files from the initial post. Everything else is generated.
>
> Heribert.
>
Re: Strange behavior with substitution groups [message #1077514 is a reply to message #1077290] Thu, 01 August 2013 20:41 Go to previous message
Heribert  Schütz is currently offline Heribert Schütz
Messages: 24
Registered: July 2009
Junior Member
Hi Ed,

the bug is filed: https://bugs.eclipse.org/bugs/show_bug.cgi?id=414251

Thanks for your hints on a cleaner work-around to the problem. I actually placed it in my derived factory so that I need not deal with a modified version of a generated file in version control and the build process.

Regards,
Heribert.
Previous Topic:Refactor URI to separate bundle?
Next Topic:[Teneo] table_per_class and id generation tips
Goto Forum:
  


Current Time: Wed Oct 22 12:32:15 GMT 2014

Powered by FUDForum. Page generated in 0.05590 seconds
.:: Contact :: Home ::.

Powered by: FUDforum 3.0.2.
Copyright ©2001-2010 FUDforum Bulletin Board Software