Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

Introduction to NIEM and IEPDs

, , 1 Jan 2009
Developed by the Department of Justice and the Department of Homeland Security, the National Information Exchange Model "Bridges Information Systems".
IepdBrowser.zip
IepdBrowser
bin
Debug
Clifton.Tools.Strings.dll
Clifton.Tools.Strings.pdb
IepdBrowser.exe
IepdBrowser.pdb
IepdBrowser.vshost.exe
IepdBrowser.vshost.exe.manifest
IepdParser.vshost.exe.manifest
CVS
Entries
Entries.Extra
Entries.Extra.Old
Entries.Old
Repository
Root
obj
Debug
IepdBrowser.csproj.GenerateResource.Cache
IepdBrowser.exe
IepdBrowser.IepdBrowserForm.resources
IepdBrowser.pdb
IepdBrowser.Properties.Resources.resources
IepdParser.csproj.GenerateResource.Cache
Refactor
ResolveAssemblyReference.cache
TempPE
Properties.Resources.Designer.cs.dll
Properties
CVS
Entries
Entries.Extra
Entries.Extra.Old
Entries.Old
Repository
Root
Settings.settings
// (c) 2009 Marc Clifton
// All Rights Reserved.
// You are free to use this code as long as you comply with the
// Code Project Open License (CPOL) 1.02
// http://www.codeproject.com/info/cpol10.aspx

/*
 * History:
 * 1/2/2009
 * Fixed problem with niem-core production release, stack overflow processing imports because they are circular.
 * Fixed exception when facet documentation doesn't exist.
 * Fixed bug where type tree wasn't being cleared when opening a new schema.
 * 1/3/2009
 * Substitution group map now allows for multiple substitution groups (see niem-core.xsd, EntityRepresentation type).
 * Fixed circular drill into of elements, such as in niem-core, DNAType, ImageLocation, LocationType, ends up with EntityRepresentation that has several substitution groups.
 * 2/11/2009
 * Stopping circular references now uses the element type qualified name, not the element qualified name, as the element qualified name is often "specialized" whereas the type it references is common.
 * Now pops "drilled into" element type qnames, because this was preventing sibling and higher level types from drilling into a type already encountered (we want to stop circular references only)
 * 2/12/2009
 * Modified cardinality to use (n..m) format.
 * Fixed: ex: DocumentType, DocumentFiledDate, should show Type=DateType, not DocumentFiledDate (we need the type of the ref)
 * Fixed: DateRepresentation is abstract and has substitution groups of simple type.  These now show up in the elements tree.
 * Type hierarchy tree is now sorted.
 * Changed hierarchy tree to iterate through all global complex types rather than starting with global elements and iterating through their types.
 *		This gives us a complete list of global types.
 * Abstact types now show "anyType" for their type.  For some reason, the IsAbstract flag isn't being set (see DateRepresentation)
 * For simple type substitution groups, now nulls out the simpleContentType after setting the simple type substitution group.
 *		This fixes a problem with showing the wrong type name in the tree.
 *	2/16/2009
 *	Element browser now drills into child elements when the node is expanded.  This greatly improves the performance of the UI, as 
 *		some of these types of very extensive element trees.
 * 2/18/2009
 * Fixed:
 *		LocationType/.../LocationState: there are 9 substitution groups.
 *			Where is it getting the list from?
 *			why does it show the list at the root element of the substitution group?
 *			Should we really show the list if available in the substitution group (code group?) ?
 *		Now shows the correct code list for the simple type of the substitution group.
 *		
 * TODO:
 *		PersonNameSuffixText: type=TextType, displayed is "string", which is TextType's type.  Contrast with DocumentType.DocumentFiledDate above.
 * 
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;

using Clifton.Tools.Strings;

namespace IepdBrowser
{
	public struct SubstElementsInfo
	{
		public XmlSchemaElement xseSubstGroup;
		public List<ElementInfo> substElements;

		public SubstElementsInfo(XmlSchemaElement xseSubstGroup, List<ElementInfo> substElements)
		{
			this.xseSubstGroup = xseSubstGroup;
			this.substElements = substElements;
		}
	}

	public class EnumFacet
	{
		protected string val;
		protected string doc;

		public string Value
		{
			get { return val; }
		}

		public string Documentation
		{
			get { return doc; }
		}

		public EnumFacet(string val, string doc)
		{
			this.val = val;
			this.doc = doc;
		}
	}

	public struct QName
	{
		public string name;
		public string ns;

		public QName(string qname)
		{
			name = StringHelpers.RightOfRightmostOf(qname, ':');
			ns = StringHelpers.LeftOfRightmostOf(qname, ':');
		}

		public QName(string name, string ns)
		{
			this.name = name;
			this.ns = ns;
		}

		public QName(XmlQualifiedName qname)
		{
			name = qname.Name;
			ns = qname.Namespace;
		}
	}

	public class ElementInfo
	{
		protected string minOccursString;
		protected string maxOccursString;
		protected string name;
		protected XmlSchemaType type;
		protected XmlQualifiedName refName;
		protected XmlSchemaElement xse;
		protected List<ElementInfo> childElements;
		protected XmlQualifiedName simpleContentType;
		protected List<SubstElementsInfo> substGroupElements;

		public string Name
		{
			get { return name; }
		}

		public string MinOccurs
		{
			get { return minOccursString; }
		}

		public string MaxOccurs
		{
			get { return maxOccursString; }
		}

		public XmlSchemaType SchemaType
		{
			get { return type; }
		}

		public string Type
		{
			get 
			{
				string ret=String.Empty;

				if (simpleContentType != null)
				{
					ret = simpleContentType.Name;
				}
				else if (type != null)
				{
					ret = type.QualifiedName.Name;
				}
				else if (refName != null)
				{
					ret = refName.Name;
				}

				return ret;
			}
		}

		public XmlQualifiedName SimpleContentType
		{
			get { return simpleContentType; }
		}

		public XmlQualifiedName RefName
		{
			get { return refName; }
		}

		public List<ElementInfo> ChildElements
		{
			get { return childElements; }
		}

		public List<SubstElementsInfo> SubstGroupElements
		{
			get { return substGroupElements; }
		}

		public ElementInfo(XmlSchemaElement xmlSchemaElement, string minOccursString, string maxOccursString, string name, XmlSchemaType type, XmlQualifiedName refName)
		{
			xse = xmlSchemaElement;
			this.minOccursString = minOccursString;
			this.maxOccursString = maxOccursString;
			this.name = name;
			this.type = type;
			this.refName = refName;
			substGroupElements = new List<SubstElementsInfo>();
			childElements = new List<ElementInfo>();

			if (maxOccursString==decimal.MaxValue.ToString())
			{
				this.maxOccursString = "*";
			}
		}

		public void DrillIntoType(Iepd iepd)
		{
			childElements = iepd.GetElements(new QName(refName), out simpleContentType);
		}

		public bool HasChildElements(Iepd iepd)
		{
			bool ret = iepd.HasChildElements(new QName(refName));

			return ret;
		}

		public void DrillIntoSubstGroup(Iepd iepd, XmlSchemaElement xseSubstGroup)
		{
			List<ElementInfo> substElements = iepd.GetElements(new QName(xseSubstGroup.QualifiedName), out simpleContentType);

			if (substElements.Count > 0)
			{
				substGroupElements.Add(new SubstElementsInfo(xseSubstGroup, substElements));
			}
			else if (simpleContentType != null)
			{
				XmlSchemaElement xse=iepd.GetSchemaElement(xseSubstGroup.QualifiedName);
				// Get the type of the type.  NIEM model specific, as types reference global types.
				XmlSchemaType xst = iepd.GetSchemaType(xse.SchemaTypeName);
				ElementInfo elInfo=new ElementInfo(xse, xse.MinOccurs.ToString(), xse.MaxOccurs.ToString(), xse.Name, xst, xse.RefName);
				substElements.Add(elInfo);
				substGroupElements.Add(new SubstElementsInfo(xseSubstGroup, substElements));
				simpleContentType = null;
			}
		}
	}

	public class Iepd
	{
		protected XmlSchema xsd;
		protected XmlSchemaSet xsdSet;
		protected Dictionary<QName, List<XmlSchemaType>> typeChildren;
		protected Dictionary<QName, QName> parentList;
		protected Dictionary<XmlQualifiedName, List<XmlSchemaElement>> substGroupMap;
		protected List<XmlSchema> schemaImports;

		public Dictionary<QName, List<XmlSchemaType>> TypeHierarchy
		{
			get { return typeChildren; }			  
		}

		public Dictionary<QName, QName> ParentList
		{
			get { return parentList; }
		}

		public Iepd(string filename)
		{
			XmlTextReader xtr = new XmlTextReader(filename);
			xsd = XmlSchema.Read(xtr, ValidationHandler);
			xsdSet = new XmlSchemaSet();
			xsdSet.Add(xsd);
			xsdSet.Compile();
			typeChildren = new Dictionary<QName, List<XmlSchemaType>>();
			parentList = new Dictionary<QName, QName>();
			substGroupMap = new Dictionary<XmlQualifiedName, List<XmlSchemaElement>>();
			schemaImports = new List<XmlSchema>();
			BuildSchemaList(xsd);
			BuildSubstitutionGroupDictionary();
		}

		public void BuildTypeHierarchyFromElements()
		{
			foreach (DictionaryEntry de in xsd.SchemaTypes)  // was xsd.Elements
			{
				//XmlSchemaElement xse = de.Value as XmlSchemaElement;
				//XmlSchemaType xst = xse.ElementSchemaType;
				//FindParent(xst);
				FindParent((XmlSchemaType)de.Value);
			}
		}

		public List<ElementInfo> GetElements(QName qname)
		{
			XmlQualifiedName notUsed;
			// Prevent infinite recursion when drilling into elements.

			return GetElements(qname, out notUsed);
		}

		public bool HasChildElements(QName qname)
		{
			bool ret = false;

			List<ElementInfo> elInfoList = new List<ElementInfo>();
			XmlSchemaType xst = GetSchemaType(qname);

			if (xst == null)
			{
				XmlSchemaElement xse = GetSchemaElement(qname);

				if (xse != null)
				{
					xst = GetSchemaType(new QName(xse.SchemaTypeName));
				}
			}

			if (xst is XmlSchemaComplexType)
			{
				XmlSchemaComplexType xsct = (XmlSchemaComplexType)xst;

				if (xsct.ContentModel is XmlSchemaComplexContent)
				{
					XmlSchemaComplexContent xscc = (XmlSchemaComplexContent)xsct.ContentModel;

					if (xscc.Content is XmlSchemaComplexContentExtension)
					{
						XmlSchemaComplexContentExtension xscce = (XmlSchemaComplexContentExtension)xscc.Content;

						if (xscce.Particle is XmlSchemaSequence)
						{
							XmlSchemaSequence xss = (XmlSchemaSequence)xscce.Particle;

							foreach (XmlSchemaObject xso in xss.Items)
							{
								if (xso is XmlSchemaElement)
								{
									ret = true;
									break;
								}
							}
						}
					}
				}
			}

			return ret;
		}

		/// <summary>
		/// Returns only the child elements.  It does not recurse, because this makes the UI unresponsive.
		/// Instead, when the user requests child elements, the UI should call back to get the child elements
		/// at the time of the request.
		/// </summary>
		public List<ElementInfo> GetElements(QName qname, out XmlQualifiedName simpleContentType)
		{
			simpleContentType = null;
			List<ElementInfo> elInfoList = new List<ElementInfo>();
			XmlSchemaType xst = GetSchemaType(qname);

			if (xst == null)
			{
				XmlSchemaElement xse = GetSchemaElement(qname);

				if (xse != null)
				{
					xst = GetSchemaType(new QName(xse.SchemaTypeName));
				}
			}

			if (xst is XmlSchemaComplexType)
			{
				XmlSchemaComplexType xsct = (XmlSchemaComplexType)xst;

				if (xsct.ContentModel is XmlSchemaComplexContent)
				{
					XmlSchemaComplexContent xscc = (XmlSchemaComplexContent)xsct.ContentModel;

					if (xscc.Content is XmlSchemaComplexContentExtension)
					{
						XmlSchemaComplexContentExtension xscce = (XmlSchemaComplexContentExtension)xscc.Content;

						if (xscce.Particle is XmlSchemaSequence)
						{
							XmlSchemaSequence xss = (XmlSchemaSequence)xscce.Particle;

							foreach (XmlSchemaObject xso in xss.Items)
							{
								if (xso is XmlSchemaElement)
								{
									XmlSchemaElement xse = (XmlSchemaElement)xso;

									// This is NIEM architecture specific, as complex types always reference a global element.
									// We should check if RefName exists to work more generally with other schemas.
									// Get the type of the referenced element

									XmlSchemaElement xseRef = GetSchemaElement(xse.RefName);
									ElementInfo elInfo = new ElementInfo(xse, xse.MinOccurs.ToString(), xse.MaxOccurs.ToString(), xse.RefName.Name, xseRef.ElementSchemaType, xse.RefName);
									elInfoList.Add(elInfo);

									if (xse.ElementSchemaType is XmlSchemaComplexType)
									{
										//Debug.WriteLine(drilledIntoElements.Count.ToString() + " " + elInfo.Name);
										// elInfo.DrillIntoType(this);

										// Is the element a substitution group?
										if (substGroupMap.ContainsKey(xse.QualifiedName))
										{
											foreach (XmlSchemaElement xseSubstGroup in substGroupMap[xse.QualifiedName])
											{
												elInfo.DrillIntoSubstGroup(this, xseSubstGroup);
											}
										}
									}
								}
							}
						}
					}
				}
				else if (xsct.ContentModel is XmlSchemaSimpleContent)
				{
					XmlSchemaSimpleContent xssc = (XmlSchemaSimpleContent)xsct.ContentModel;

					if (xssc.Content is XmlSchemaSimpleContentExtension)
					{
						XmlSchemaSimpleContentExtension xssce = (XmlSchemaSimpleContentExtension)xssc.Content;
						simpleContentType = xssce.BaseTypeName;
					}
				}
			}

			return elInfoList;
		}

		public bool HasChildElements(ElementInfo elInfo)
		{
			bool ret = elInfo.HasChildElements(this);

			return ret;
		}

		protected void FindParent(XmlSchemaType xst)
		{
			if (xst is XmlSchemaComplexType)
			{
				XmlSchemaComplexType xsct = (XmlSchemaComplexType)xst;

				if (xsct.ContentModel is XmlSchemaComplexContent)
				{
					XmlSchemaComplexContent xscc = (XmlSchemaComplexContent)xsct.ContentModel;

					if (xscc.Content is XmlSchemaComplexContentExtension)
					{
						XmlQualifiedName baseType = ((XmlSchemaComplexContentExtension)xscc.Content).BaseTypeName;
						QName parentKey = new QName(baseType);

						if (!typeChildren.ContainsKey(parentKey))
						{
							typeChildren[parentKey] = new List<XmlSchemaType>();
						}

						if (!typeChildren[parentKey].Contains(xst))
						{
							typeChildren[parentKey].Add(xst);
							QName childQName = new QName(xst.QualifiedName);
							parentList[childQName] = parentKey;
							XmlSchemaType xstParent = FindType(baseType);
							FindParent(xstParent);
						}
					}
				}
			}
		}

		protected XmlSchemaType FindType(XmlQualifiedName baseType)
		{
			XmlSchemaType xstParent = null;

			foreach (XmlSchema schema in schemaImports)
			{
				xstParent = schema.SchemaTypes[baseType] as XmlSchemaType;

				if (xstParent != null)
				{
					break;
				}
			}

			return xstParent;
		}

		public void GetElements()
		{
			XmlSchemaObjectTable xsot = xsd.Elements;

			foreach (DictionaryEntry de in xsot)
			{
				string qname = de.Key.ToString();
				XmlQualifiedName xqname = GetXmlQualifiedName(qname);
				XmlSchemaElement xse = (XmlSchemaElement)xsd.Elements[xqname];

				XmlSchemaType xst = (XmlSchemaType)xsd.SchemaTypes[GetXmlQualifiedName(xse.SchemaTypeName.Name, xse.SchemaTypeName.Namespace)];

				if (xst is XmlSchemaComplexType)
				{
					XmlSchemaComplexType xsct = (XmlSchemaComplexType)xst;

					if (xsct.ContentModel is XmlSchemaComplexContent)
					{
						XmlSchemaComplexContent xscc = (XmlSchemaComplexContent)xsct.ContentModel;

						if (xscc.Content is XmlSchemaComplexContentExtension)
						{
							XmlQualifiedName baseType = ((XmlSchemaComplexContentExtension)xscc.Content).BaseTypeName;
						}
					}
				} 
			}
		}

		public XmlSchemaType GetSchemaType(QName qname)
		{
			XmlQualifiedName xqn=GetXmlQualifiedName(qname);

			return GetSchemaType(xqn);
		}

		public XmlSchemaType GetSchemaType(XmlQualifiedName xqn)
		{
			XmlSchemaType xst = null;

			foreach (XmlSchema xsd in schemaImports)
			{
				xst = xsd.SchemaTypes[xqn] as XmlSchemaType;

				if (xst != null)
				{
					break;
				}
			}

			return xst;
		}

		public XmlSchemaElement GetSchemaElement(QName qname)
		{
			XmlQualifiedName xqn = GetXmlQualifiedName(qname);

			return GetSchemaElement(xqn);
		}

		public XmlSchemaElement GetSchemaElement(XmlQualifiedName xqn)
		{
			XmlSchemaElement xse = xsd.Elements[xqn] as XmlSchemaElement;

			if (xse == null)
			{
				foreach (XmlSchemaImport import in xsd.Includes)
				{
					xse = import.Schema.Elements[xqn] as XmlSchemaElement;

					if (xse != null)
					{
						break;
					}
				}
			}

			return xse;
		}

		public void GetTypes()
		{
			List<XmlQualifiedName> types = new List<XmlQualifiedName>();

			XmlSchemaObjectTable xsot = xsd.SchemaTypes;
		}

		public List<EnumFacet> GetLookupData(ElementInfo elInfo)
		{
			List<EnumFacet> enums = new List<EnumFacet>();

			if (elInfo != null)
			{
				if (elInfo.SimpleContentType != null)
				{
					XmlSchemaType xse = GetSchemaType(elInfo.SimpleContentType);

					if (xse is XmlSchemaSimpleType)
					{
						XmlSchemaSimpleType xsst = xse as XmlSchemaSimpleType;
						GetLookupData(enums, xsst);
					}
				}
				else
				{
					XmlSchemaType xst = elInfo.SchemaType;

					if (xst.BaseXmlSchemaType is XmlSchemaSimpleType)
					{
						XmlSchemaSimpleType xsst = xst.BaseXmlSchemaType as XmlSchemaSimpleType;
						GetLookupData(enums, xsst);
					}
				}
				//else
				//{
				//    // Find an element with this type as the substitutionGroup
				//    XmlQualifiedName elxqn = elInfo.RefName;
				//    XmlSchemaElement xse = FindFirstSubstitutionGroupElement(elxqn);

				//    if (xse != null)
				//    {
				//        if (xse.ElementSchemaType is XmlSchemaComplexType)
				//        {
				//            XmlSchemaComplexType xsct = xse.ElementSchemaType as XmlSchemaComplexType;

				//            if (xsct.ContentModel is XmlSchemaSimpleContent)
				//            {
				//                XmlSchemaSimpleContent xssc = xsct.ContentModel as XmlSchemaSimpleContent;

				//                if (xssc.Content is XmlSchemaSimpleContentExtension)
				//                {
				//                    XmlSchemaSimpleContentExtension xssce = (XmlSchemaSimpleContentExtension)xssc.Content;
				//                    XmlQualifiedName xqn = xssce.BaseTypeName;
				//                    XmlSchemaType xse2 = GetSchemaType(xqn);

				//                    if (xse2 is XmlSchemaSimpleType)
				//                    {
				//                        XmlSchemaSimpleType xsst = xse2 as XmlSchemaSimpleType;
				//                        GetLookupData(enums, xsst);
				//                    }
				//                }
				//            }
				//        }
				//    }
				//}
			}

			return enums;
		}

		/// <summary>
		/// Returns a list of the XmlSchemaSimpleType enumeration facet (including any annotation documentation) values.
		/// </summary>
		protected void GetLookupData(List<EnumFacet> enums, XmlSchemaSimpleType xsst)
		{
			if (xsst.Content is XmlSchemaSimpleTypeRestriction)
			{
				XmlSchemaSimpleTypeRestriction xsstr = xsst.Content as XmlSchemaSimpleTypeRestriction;

				if (xsstr.Facets != null)
				{
					foreach (XmlSchemaObject xso in xsstr.Facets)
					{
						if (xso is XmlSchemaEnumerationFacet)
						{
							XmlSchemaEnumerationFacet xsef = xso as XmlSchemaEnumerationFacet;
							string doc = GetDocumentation(xsef.Annotation);
							string val = xsef.Value;
							enums.Add(new EnumFacet(val, doc));
						}
					}
				}
			}
		}

		/// <summary>
		/// Get a unique list of all the schemas being imported, recursively.
		/// We do this because the niem-core schema circular imports, and this
		/// caused a stack overflow in the first version of the browser.
		/// </summary>
		protected void BuildSchemaList(XmlSchema xsd)
		{
			schemaImports.Add(xsd);

			foreach (XmlSchemaImport import in xsd.Includes)
			{
				if (!schemaImports.Contains(import.Schema))
				{
					BuildSchemaList(import.Schema);
				}
			}
		}

		protected void BuildSubstitutionGroupDictionary()
		{
			foreach (XmlSchema xsd in schemaImports)
			{
				foreach (XmlSchemaObject xso in xsd.Items)
				{
					if (xso is XmlSchemaElement)
					{
						XmlSchemaElement xse = xso as XmlSchemaElement;

						if (xse.SubstitutionGroup != null)
						{
							// The substitution group is the key, the element that redefines
							// the qualified name is the value.
							if (!substGroupMap.ContainsKey(xse.SubstitutionGroup))
							{
								substGroupMap[xse.SubstitutionGroup] = new List<XmlSchemaElement>();
							}

							substGroupMap[xse.SubstitutionGroup].Add(xse);
						}
					}
				}
			}
		}

		/// <summary>
		/// Returns the global element (or null) whose substitutionGroup is the specified XmlQualifiedName.
		/// Returns the first element in the potential list of substitution groups.
		/// </summary>
		protected XmlSchemaElement FindFirstSubstitutionGroupElement(XmlQualifiedName elxqn)
		{
			List<XmlSchemaElement> elemList = null;
			XmlSchemaElement elem = null;
			substGroupMap.TryGetValue(elxqn, out elemList);

			if (elemList != null)
			{
				elem = elemList[0];
			}

			return elem;
		}

		protected string GetDocumentation(XmlSchemaAnnotation xsa)
		{
			StringBuilder sb = new StringBuilder();

			if (xsa != null)
			{
				foreach (XmlSchemaObject xso in xsa.Items)
				{
					if (xso is XmlSchemaDocumentation)
					{
						XmlSchemaDocumentation xsd = xso as XmlSchemaDocumentation;

						foreach (XmlNode xn in xsd.Markup)
						{
							sb.Append(xn.InnerText);
							sb.Append("\r\n");
						}
					}
				}
			}

			return sb.ToString();
		}

		protected void ValidationHandler(object sender, ValidationEventArgs args)
		{
			Debug.WriteLine(args.Message);			
		}

		protected XmlQualifiedName GetXmlQualifiedName(string qname)
		{
			string name = StringHelpers.RightOfRightmostOf(qname, ':');
			string ns = StringHelpers.LeftOfRightmostOf(qname, ':');

			return new XmlQualifiedName(name, ns);
		}

		protected XmlQualifiedName GetXmlQualifiedName(string name, string ns)
		{
			return new XmlQualifiedName(name, ns);
		}

		protected XmlQualifiedName GetXmlQualifiedName(QName qname)
		{
			return new XmlQualifiedName(qname.name, qname.ns);
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Authors

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Mark Long
Database Developer
United States United States
Educated in industrial engineering, optimization techniques, information security, computer science, math, statistics, and business management, I support a public safety software platform.
 
Keywords:
Information Systems, relational database management systems, information security, ERP, EAI, SAP, PeopleSoft, data warehousing, OFX, e-commerce, speech recognition, CRM, statistical analysis, modeling and simulation, operations research, cognitive engineering.

| Advertise | Privacy | Mobile
Web03 | 2.8.140827.1 | Last Updated 1 Jan 2009
Article Copyright 2009 by Marc Clifton, Mark Long
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid