/**
* Copyright 2014 Mike Pigott
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mpigott.sql.xml;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.transform.stream.StreamSource;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaUse;
import org.apache.ws.commons.schema.docpath.XmlSchemaStateMachineGenerator;
import org.apache.ws.commons.schema.docpath.XmlSchemaStateMachineNode;
import org.apache.ws.commons.schema.walker.XmlSchemaAttrInfo;
import org.apache.ws.commons.schema.walker.XmlSchemaTypeInfo;
import org.apache.ws.commons.schema.walker.XmlSchemaWalker;
/**
* Builds SQL from an XML.
*
* @author Mike Pigott
*/
public class SqlSchemaGenerator {
private static final String CAMEL_CASE_REGEX =
"(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])";
XmlSchemaCollection xmlSchemaCollection;
XmlSchemaStateMachineNode rootNode;
Map<QName, XmlSchemaStateMachineNode> stateMachineNodesByQName;
public SqlSchemaGenerator(SqlXmlConfig config) throws IOException {
xmlSchemaCollection = new XmlSchemaCollection();
for (StreamSource source : config.getSources()) {
xmlSchemaCollection.read(source);
// Close the streams.
if (source.getInputStream() != null) {
try {
source.getInputStream().close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
if (source.getReader() != null) {
try {
source.getReader().close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
XmlSchemaStateMachineGenerator stateMachineGen =
new XmlSchemaStateMachineGenerator();
XmlSchemaWalker walker =
new XmlSchemaWalker(xmlSchemaCollection, stateMachineGen);
walker.setUserRecognizedTypes(SqlType.getRecognizedTypes());
XmlSchemaElement rootElem =
xmlSchemaCollection.getElementByQName(config.getRootTagName());
walker.walk(rootElem);
rootNode = stateMachineGen.getStartNode();
stateMachineNodesByQName = stateMachineGen.getStateMachineNodesByQName();
}
public XmlSchemaStateMachineNode getStateMachine() {
return rootNode;
}
public Map<QName, XmlSchemaStateMachineNode> getStateMachineNodesByQName() {
return stateMachineNodesByQName;
}
public SqlSchema generate(SqlXmlConfig config) throws IOException {
SqlSchema sqlSchema = new SqlSchema();
final List<XmlSchemaAttrInfo> rootAttrs = rootNode.getAttributes();
if ((rootAttrs != null) && !rootAttrs.isEmpty()) {
for (XmlSchemaStateMachineNode child : rootNode.getPossibleNextStates()) {
createSchemaFor(sqlSchema, child, null, null, null);
}
} else {
createSchemaFor(sqlSchema, rootNode, null, null, null);
}
return sqlSchema;
}
private void createSchemaFor(
SqlSchema schema,
XmlSchemaStateMachineNode node,
SqlTable parentTable,
SqlRelationship relationshipToParent,
List<Integer> pathFromParent) {
final boolean isElement =
node.getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT);
final boolean isElementOrAny =
(isElement
|| node.getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY));
final boolean newTableRequired =
isElementOrAny
&& ((parentTable == null)
|| (isElement
&& node
.getElementType()
.getType()
.equals(XmlSchemaTypeInfo.Type.COMPLEX))
|| !relationshipToParent.equals(SqlRelationship.ONE_TO_ONE));
switch (node.getNodeType()) {
case ELEMENT:
{
if (newTableRequired) {
createTablesFor(
schema,
node,
parentTable,
relationshipToParent,
pathFromParent);
} else {
final SqlAttribute attr =
new SqlAttribute(
getSqlNameFor(node.getElement().getQName()),
node.getElementType());
parentTable.addAttribute(attr);
}
break;
}
case SUBSTITUTION_GROUP:
case CHOICE:
{
/* TODO: These are one-to-many relationships,
* unless there is only one choice.
*/
break;
}
case SEQUENCE:
case ALL:
{
/* TODO: These are one-to-one relationships,
* unless their children are of other
* group types.
*/
break;
}
case ANY:
{
if (newTableRequired) {
createTablesFor(
schema,
node,
parentTable,
relationshipToParent,
pathFromParent);
} else {
parentTable.addAttribute(new SqlAttribute(node.getAny()));
}
break;
}
default:
}
}
private void createTablesFor(SqlSchema schema,
XmlSchemaStateMachineNode node,
SqlTable parentTable,
SqlRelationship relationshipToParent,
List<Integer> pathFromParent) {
// TODO: Handle ANYs.
final SqlTable table =
new SqlTable(
getSqlNameFor( node.getElement().getQName() ),
pathFromParent);
if (parentTable != null) {
parentTable.addRelationship(relationshipToParent, table);
}
final List<Integer> pathFromNewTable = new ArrayList<Integer>();
for (int nextPathIdx = 0;
nextPathIdx < node.getPossibleNextStates().size();
++nextPathIdx) {
pathFromNewTable.add(nextPathIdx);
createSchemaFor(
schema,
node,
table,
SqlRelationship.ONE_TO_ONE,
pathFromNewTable);
pathFromNewTable.remove(pathFromNewTable.size() - 1);
}
}
private static String getSqlNameFor(QName qName) {
StringBuilder sqlName = new StringBuilder( qName.getLocalPart().length() );
final String[] parts = qName.getLocalPart().split(CAMEL_CASE_REGEX);
for (int partIdx = 0; partIdx < parts.length - 1; ++partIdx) {
sqlName.append(parts[partIdx]).append('_');
}
sqlName.append(parts[parts.length - 1]);
// TODO: handle all other special characters.
return sqlName.toString();
}
private static void validateXmlTypeForSqlAttribute(
XmlSchemaTypeInfo xmlTypeInfo) {
switch(xmlTypeInfo.getType()) {
case COMPLEX:
throw new IllegalArgumentException(
"Cannot create an SqlAttribute from a complex element.");
case UNION:
throw new IllegalArgumentException(
"Cannot create an SqlAttribute from a union of types.");
case LIST:
{
final XmlSchemaTypeInfo.Type listType =
xmlTypeInfo.getChildTypes().get(0).getType();
if (listType.equals(XmlSchemaTypeInfo.Type.UNION)) {
throw new IllegalArgumentException(
"Cannot create an SqlAttribute from "
+ "a list of union of types.");
}
}
/* falls through */
default:
/* falls through */
}
}
private static SqlAttribute createAttribute(
XmlSchemaStateMachineNode stateMachine) {
if ((stateMachine.getMinOccurs() < 0L)
|| (stateMachine.getMinOccurs() > stateMachine.getMaxOccurs())) {
throw new IllegalStateException(
"Min occurs ("
+ stateMachine.getMinOccurs()
+ ") must be greater than zero and less-than-or-equal-to max occurs ("
+ stateMachine.getMaxOccurs()
+ ").");
}
if (stateMachine.getMaxOccurs() > 1L) {
throw new IllegalArgumentException(
"Cannot create an SqlAttribute from an element or any with a "
+ "max-occurs greater than one.");
}
// Sanity checks.
switch (stateMachine.getNodeType()) {
case ANY:
{
// This will just be an SQLXML type, only minor checking required.
return new SqlAttribute(
"any",
SqlType.SQLXML,
false,
(stateMachine.getMinOccurs() == 0));
}
case ELEMENT:
{
// Must not be complex, and must not have any attributes.
if ((stateMachine.getAttributes() != null)
&& !stateMachine.getAttributes().isEmpty()) {
throw new IllegalArgumentException(
"Cannot create an SqlAttribute from an element with attributes.");
}
break;
}
default:
throw new IllegalArgumentException(
"Cannot create an SqlAttribute from a " + stateMachine.getNodeType());
}
// Now build the attribute from the element.
return createAttribute(
getSqlNameFor(stateMachine.getElement().getQName()),
stateMachine.getElementType(),
stateMachine.getMinOccurs() == 0);
}
private static SqlAttribute createAttribute(XmlSchemaAttrInfo attribute) {
final XmlSchemaUse attrUse =
attribute.getAttribute().getUse();
switch (attrUse) {
case PROHIBITED:
throw new IllegalArgumentException(
"Cannot create an SqlAttribute for a prohibited XML attribute");
case NONE:
throw new IllegalArgumentException(
"Cannot create an SqlAttribute for an "
+ "XML attribute with no known use.");
default:
/* falls through */
}
return createAttribute(
getSqlNameFor(attribute.getAttribute().getQName()),
attribute.getType(),
attrUse.equals(XmlSchemaUse.OPTIONAL));
}
private static SqlAttribute createAttribute(
String name,
XmlSchemaTypeInfo xmlTypeInfo,
boolean isOptional) {
validateXmlTypeForSqlAttribute(xmlTypeInfo);
final boolean isArray =
xmlTypeInfo
.getType()
.equals(XmlSchemaTypeInfo.Type.LIST);
QName xmlTypeToConvert = xmlTypeInfo.getUserRecognizedType();
if (isArray) {
xmlTypeToConvert =
xmlTypeInfo
.getChildTypes()
.get(0)
.getUserRecognizedType();
}
final SqlType sqlType = SqlType.getSqlTypeFor(xmlTypeToConvert);
return new SqlAttribute(
name,
sqlType,
isArray,
isOptional);
}
}