You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/07/25 12:25:23 UTC

[08/16] cayenne git commit: CAY-2335: New XML loading/saving mechanics with support of plugable handlers - new XML loader for DataMap - new project version - updated test projects

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbEntityHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbEntityHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbEntityHandler.java
new file mode 100644
index 0000000..8de6bb1
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbEntityHandler.java
@@ -0,0 +1,146 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class DbEntityHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String DB_ENTITY_TAG = "db-entity";
+    private static final String DB_ATTRIBUTE_TAG = "db-attribute";
+    private static final String DB_KEY_GENERATOR_TAG = "db-key-generator";
+    private static final String QUALIFIER_TAG = "qualifier";
+
+    private DataMap dataMap;
+    private DbEntity entity;
+    private DbAttribute lastAttribute;
+
+    public DbEntityHandler(NamespaceAwareNestedTagHandler parentHandler, DataMap dataMap) {
+        super(parentHandler);
+        this.dataMap = dataMap;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        switch (localName) {
+            case DB_ENTITY_TAG:
+                createDbEntity(attributes);
+                return true;
+
+            case DB_ATTRIBUTE_TAG:
+                createDbAttribute(attributes);
+                return true;
+
+            case QUALIFIER_TAG:
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void processCharData(String localName, String data) {
+        switch (localName) {
+            case QUALIFIER_TAG:
+                createQualifier(data);
+                break;
+        }
+    }
+
+    @Override
+    protected ContentHandler createChildTagHandler(String namespaceURI, String localName, String qName, Attributes attributes) {
+        switch (localName) {
+            case DB_KEY_GENERATOR_TAG:
+                return new DbKeyGeneratorHandler(this, entity);
+        }
+        return super.createChildTagHandler(namespaceURI, localName, qName, attributes);
+    }
+
+    private void createDbEntity(Attributes attributes) {
+        String name = attributes.getValue("name");
+        entity = new DbEntity(name);
+        entity.setSchema(attributes.getValue("schema"));
+        entity.setCatalog(attributes.getValue("catalog"));
+        dataMap.addDbEntity(entity);
+    }
+
+    private void createDbAttribute(Attributes attributes) {
+        String name = attributes.getValue("name");
+        String type = attributes.getValue("type");
+
+        lastAttribute = new DbAttribute(name);
+        lastAttribute.setType(TypesMapping.getSqlTypeByName(type));
+        entity.addAttribute(lastAttribute);
+
+        String length = attributes.getValue("length");
+        if (length != null) {
+            lastAttribute.setMaxLength(Integer.parseInt(length));
+        }
+
+        String precision = attributes.getValue("attributePrecision");
+        if (precision != null) {
+            lastAttribute.setAttributePrecision(Integer.parseInt(precision));
+        }
+
+        // this is an obsolete 1.2 'precision' attribute that really meant 'scale'
+        String pseudoPrecision = attributes.getValue("precision");
+        if (pseudoPrecision != null) {
+            lastAttribute.setScale(Integer.parseInt(pseudoPrecision));
+        }
+
+        String scale = attributes.getValue("scale");
+        if (scale != null) {
+            lastAttribute.setScale(Integer.parseInt(scale));
+        }
+
+        lastAttribute.setPrimaryKey(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("isPrimaryKey")));
+        lastAttribute.setMandatory(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("isMandatory")));
+        lastAttribute.setGenerated(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("isGenerated")));
+    }
+
+    private void createQualifier(String qualifier) {
+        if (qualifier.trim().length() == 0) {
+            return;
+        }
+
+        // qualifier can belong to ObjEntity, DbEntity or a query
+        if (entity != null) {
+            entity.setQualifier(ExpressionFactory.exp(qualifier));
+        }
+    }
+
+    public DbEntity getEntity() {
+        return entity;
+    }
+
+    public DbAttribute getLastAttribute() {
+        return lastAttribute;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbKeyGeneratorHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbKeyGeneratorHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbKeyGeneratorHandler.java
new file mode 100644
index 0000000..e76c4c3
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbKeyGeneratorHandler.java
@@ -0,0 +1,117 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbKeyGenerator;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class DbKeyGeneratorHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String DB_KEY_GENERATOR_TAG = "db-key-generator";
+    private static final String DB_GENERATOR_TYPE_TAG = "db-generator-type";
+    private static final String DB_GENERATOR_NAME_TAG = "db-generator-name";
+    private static final String DB_KEY_CACHE_SIZE_TAG = "db-key-cache-size";
+
+    DbEntity entity;
+
+    public DbKeyGeneratorHandler(NamespaceAwareNestedTagHandler parentHandler, DbEntity entity) {
+        super(parentHandler);
+        this.entity = entity;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        switch (localName) {
+            case DB_KEY_GENERATOR_TAG:
+                createDbKeyGenerator();
+                return true;
+
+            case DB_GENERATOR_NAME_TAG:
+            case DB_GENERATOR_TYPE_TAG:
+            case DB_KEY_CACHE_SIZE_TAG:
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void processCharData(String localName, String data) {
+        switch (localName) {
+            case DB_GENERATOR_TYPE_TAG:
+                setDbGeneratorType(data);
+                break;
+
+            case DB_GENERATOR_NAME_TAG:
+                setDbGeneratorName(data);
+                break;
+
+            case DB_KEY_CACHE_SIZE_TAG:
+                setDbKeyCacheSize(data);
+                break;
+        }
+    }
+
+    private void createDbKeyGenerator() {
+        entity.setPrimaryKeyGenerator(new DbKeyGenerator());
+    }
+
+    private void setDbGeneratorType(String type) {
+        if (entity == null) {
+            return;
+        }
+        DbKeyGenerator pkGenerator = entity.getPrimaryKeyGenerator();
+        pkGenerator.setGeneratorType(type);
+        if (pkGenerator.getGeneratorType() == null) {
+            entity.setPrimaryKeyGenerator(null);
+        }
+    }
+
+    private void setDbGeneratorName(String name) {
+        if (entity == null) {
+            return;
+        }
+        DbKeyGenerator pkGenerator = entity.getPrimaryKeyGenerator();
+        if (pkGenerator == null) {
+            return;
+        }
+        pkGenerator.setGeneratorName(name);
+    }
+
+    private void setDbKeyCacheSize(String size) {
+        if (entity == null) {
+            return;
+        }
+        DbKeyGenerator pkGenerator = entity.getPrimaryKeyGenerator();
+        if (pkGenerator == null) {
+            return;
+        }
+        try {
+            pkGenerator.setKeyCacheSize(new Integer(size.trim()));
+        } catch (Exception ex) {
+            pkGenerator.setKeyCacheSize(null);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbRelationshipHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbRelationshipHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbRelationshipHandler.java
new file mode 100644
index 0000000..b4730e5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DbRelationshipHandler.java
@@ -0,0 +1,97 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class DbRelationshipHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String DB_RELATIONSHIP_TAG = "db-relationship";
+    public static final String DB_ATTRIBUTE_PAIR_TAG = "db-attribute-pair";
+
+    private DataMap map;
+
+    private DbRelationship dbRelationship;
+
+    public DbRelationshipHandler(NamespaceAwareNestedTagHandler parentHandler, DataMap map) {
+        super(parentHandler);
+        this.map = map;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+
+        switch (localName) {
+            case DB_RELATIONSHIP_TAG:
+                createRelationship(attributes);
+                return true;
+
+            case DB_ATTRIBUTE_PAIR_TAG:
+                createDbAttributePair(attributes);
+                return true;
+        }
+
+        return false;
+    }
+
+    private void createRelationship(Attributes attributes) throws SAXException {
+        String name = attributes.getValue("name");
+        if (name == null) {
+            throw new SAXException("DbRelationshipHandler::createRelationship() - missing \"name\" attribute.");
+        }
+
+        String sourceName = attributes.getValue("source");
+        if (sourceName == null) {
+            throw new SAXException("DbRelationshipHandler::createRelationship() - null source entity");
+        }
+
+        DbEntity source = map.getDbEntity(sourceName);
+        if (source == null) {
+            return;
+        }
+
+        dbRelationship = new DbRelationship(name);
+        dbRelationship.setSourceEntity(source);
+        dbRelationship.setTargetEntityName(attributes.getValue("target"));
+        dbRelationship.setToMany(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("toMany")));
+        dbRelationship.setToDependentPK(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("toDependentPK")));
+
+        source.addRelationship(dbRelationship);
+    }
+
+    private void createDbAttributePair(Attributes attributes) {
+        DbJoin join = new DbJoin(dbRelationship);
+        join.setSourceName(attributes.getValue("source"));
+        join.setTargetName(attributes.getValue("target"));
+        dbRelationship.addJoin(join);
+    }
+
+    public DbRelationship getDbRelationship() {
+        return dbRelationship;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultDataChannelMetaData.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultDataChannelMetaData.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultDataChannelMetaData.java
new file mode 100644
index 0000000..869abed
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultDataChannelMetaData.java
@@ -0,0 +1,91 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.cayenne.configuration.ConfigurationNode;
+
+/**
+ * <p>
+ *     Default implementation of {@link DataChannelMetaData} that stores data in Map.
+ * </p>
+ * <p>
+ *     This implementation is thread safe.
+ * </p>
+ *
+ * @see NoopDataChannelMetaData
+ * @since 4.1
+ */
+public class DefaultDataChannelMetaData implements DataChannelMetaData {
+
+    private Map<ConfigurationNode, Map<Class<?>, Object>> map;
+
+    public DefaultDataChannelMetaData() {
+        map = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * value.getClass() will be used under the hood to associate data with the key object.
+     *
+     * @param key object for which we want to store data
+     * @param value data to store
+     */
+    @Override
+    public void add(ConfigurationNode key, Object value) {
+        if(key == null || value == null) {
+            return;
+        }
+
+        Map<Class<?>, Object> data = map.get(key);
+        if(data == null) {
+            data = new ConcurrentHashMap<>();
+            Map<Class<?>, Object> old = map.put(key, data);
+            // extra check in case if someone was fast enough
+            if(old != null) {
+                data.putAll(old);
+            }
+        }
+        data.put(value.getClass(), value);
+    }
+
+    /**
+     * If either key or value is {@code null} then {@code null} will be returned.
+     *
+     * @param key object for wich we want meta data
+     * @param type meta data type class
+     * @param <T> data type
+     * @return value or {@code null}
+     */
+    @Override
+    public <T> T get(ConfigurationNode key, Class<T> type) {
+        if(key == null || type == null) {
+            return null;
+        }
+
+        Map<Class<?>, Object> data = map.get(key);
+        if(data == null) {
+            return null;
+        }
+
+        return type.cast(data.get(type));
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultHandlerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultHandlerFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultHandlerFactory.java
new file mode 100644
index 0000000..5fd2293
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/DefaultHandlerFactory.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.Attributes;
+
+/**
+ * @since 4.1
+ */
+public class DefaultHandlerFactory implements HandlerFactory {
+
+    private static Logger logger = LoggerFactory.getLogger(XMLDataChannelDescriptorLoader.class);
+
+    @Override
+    public NamespaceAwareNestedTagHandler createHandler(String namespace, String localName, NamespaceAwareNestedTagHandler parent) {
+        return new NamespaceAwareNestedTagHandler(parent, namespace) {
+            @Override
+            protected boolean processElement(String namespaceURI, String localName, Attributes attributes) {
+                logger.debug("Skipping unknown tag <{}:{}>", namespaceURI, localName);
+                return true;
+            }
+        };
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableAttributeHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableAttributeHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableAttributeHandler.java
new file mode 100644
index 0000000..d0d4ae9
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableAttributeHandler.java
@@ -0,0 +1,69 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.map.EmbeddedAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class EmbeddableAttributeHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String EMBEDDED_ATTRIBUTE_TAG = "embedded-attribute";
+    private static final String EMBEDDABLE_ATTRIBUTE_OVERRIDE_TAG = "embeddable-attribute-override";
+
+    private ObjEntity entity;
+
+    private EmbeddedAttribute embeddedAttribute;
+
+    public EmbeddableAttributeHandler(NamespaceAwareNestedTagHandler parentHandler, ObjEntity entity) {
+        super(parentHandler);
+        this.entity = entity;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        switch (localName) {
+            case EMBEDDED_ATTRIBUTE_TAG:
+                createEmbeddableAttribute(attributes);
+                return true;
+
+            case EMBEDDABLE_ATTRIBUTE_OVERRIDE_TAG:
+                createEmbeddableAttributeOverride(attributes);
+                return true;
+        }
+
+        return false;
+    }
+
+    private void createEmbeddableAttribute(Attributes attributes) {
+        embeddedAttribute = new EmbeddedAttribute(attributes.getValue("name"));
+        embeddedAttribute.setType(attributes.getValue("type"));
+        entity.addAttribute(embeddedAttribute);
+    }
+
+    private void createEmbeddableAttributeOverride(Attributes attributes) {
+        embeddedAttribute.addAttributeOverride(attributes.getValue("name"),
+                attributes.getValue("db-attribute-path"));
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableHandler.java
new file mode 100644
index 0000000..648474e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/EmbeddableHandler.java
@@ -0,0 +1,75 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.Embeddable;
+import org.apache.cayenne.map.EmbeddableAttribute;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class EmbeddableHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String EMBEDDABLE_TAG = "embeddable";
+    private static final String EMBEDDABLE_ATTRIBUTE_TAG = "embeddable-attribute";
+
+    private DataMap map;
+
+    private Embeddable embeddable;
+
+    public EmbeddableHandler(NamespaceAwareNestedTagHandler parentHandler, DataMap map) {
+        super(parentHandler);
+        this.map = map;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        switch (localName) {
+            case EMBEDDABLE_TAG:
+                createEmbeddable(attributes);
+                return true;
+
+            case EMBEDDABLE_ATTRIBUTE_TAG:
+                createEmbeddableAttribute(attributes);
+                return true;
+        }
+
+        return false;
+    }
+
+    private void createEmbeddable(Attributes attributes) {
+        embeddable = new Embeddable(attributes.getValue("className"));
+        map.addEmbeddable(embeddable);
+    }
+
+    private void createEmbeddableAttribute(Attributes attributes) {
+        EmbeddableAttribute ea = new EmbeddableAttribute(attributes.getValue("name"));
+        ea.setType(attributes.getValue("type"));
+        ea.setDbAttributeName(attributes.getValue("db-attribute-name"));
+        embeddable.addAttribute(ea);
+    }
+
+    public Embeddable getEmbeddable() {
+        return embeddable;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/HandlerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/HandlerFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/HandlerFactory.java
new file mode 100644
index 0000000..d02be06
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/HandlerFactory.java
@@ -0,0 +1,30 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+/**
+ * Factory that creates handlers for unparsed elements.
+ *
+ * @since 4.1
+ */
+public interface HandlerFactory {
+
+    NamespaceAwareNestedTagHandler createHandler(String namespace, String localName, NamespaceAwareNestedTagHandler parent);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/LoaderContext.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/LoaderContext.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/LoaderContext.java
new file mode 100644
index 0000000..9507e4f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/LoaderContext.java
@@ -0,0 +1,63 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.apache.cayenne.map.DataMap;
+import org.xml.sax.XMLReader;
+
+/**
+ * @since 4.1
+ */
+public class LoaderContext {
+
+    Collection<DataMapLoaderListener> dataMapListeners;
+
+    private XMLReader xmlReader;
+
+    private HandlerFactory factory;
+
+    public LoaderContext(XMLReader reader, HandlerFactory factory) {
+        this.xmlReader = reader;
+        this.factory = factory;
+        dataMapListeners = new ArrayList<>();
+    }
+
+    public HandlerFactory getFactory() {
+        return factory;
+    }
+
+    public XMLReader getXmlReader() {
+        return xmlReader;
+    }
+
+    public void addDataMapListener(DataMapLoaderListener dataMapLoaderListener) {
+        dataMapListeners.add(dataMapLoaderListener);
+    }
+
+    public void dataMapLoaded(DataMap dataMap) {
+        for(DataMapLoaderListener listener : dataMapListeners) {
+            listener.onDataMapLoaded(dataMap);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NamespaceAwareNestedTagHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NamespaceAwareNestedTagHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NamespaceAwareNestedTagHandler.java
new file mode 100644
index 0000000..ec332e5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NamespaceAwareNestedTagHandler.java
@@ -0,0 +1,97 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import java.util.Objects;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+/**
+ * Base class for handlers that can delegate execution of unknown tags to
+ * handlers produced by factory.
+ *
+ * @since 4.1
+ */
+abstract public class NamespaceAwareNestedTagHandler extends SAXNestedTagHandler {
+
+    protected String targetNamespace;
+
+    private StringBuilder charactersBuffer = new StringBuilder();
+
+    public NamespaceAwareNestedTagHandler(LoaderContext loaderContext) {
+        super(loaderContext);
+    }
+
+    public NamespaceAwareNestedTagHandler(SAXNestedTagHandler parentHandler, String targetNamespace) {
+        super(parentHandler);
+        this.targetNamespace = Objects.requireNonNull(targetNamespace);
+    }
+
+    public NamespaceAwareNestedTagHandler(NamespaceAwareNestedTagHandler parentHandler) {
+        this(parentHandler, parentHandler.targetNamespace);
+    }
+
+    abstract protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException;
+
+    protected void processCharData(String localName, String data) {
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        charactersBuffer.append(ch, start, length);
+    }
+
+    @Override
+    public final void startElement(String namespaceURI, String localName,
+                                   String qName, Attributes attributes) throws SAXException {
+
+        ContentHandler childHandler = createChildTagHandler(namespaceURI, localName, qName, attributes);
+
+        if(!namespaceURI.equals(targetNamespace) || !processElement(namespaceURI, localName, attributes)) {
+            // recursively pass element down into child handlers
+            childHandler.startElement(namespaceURI, localName, qName, attributes);
+        }
+
+        // push child handler to the stack...
+        loaderContext.getXmlReader().setContentHandler(childHandler);
+        charactersBuffer.delete(0, charactersBuffer.length());
+    }
+
+    @Override
+    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
+        super.endElement(namespaceURI, localName, qName);
+        if(namespaceURI.equals(targetNamespace) && parentHandler instanceof NamespaceAwareNestedTagHandler) {
+            ((NamespaceAwareNestedTagHandler)parentHandler).processCharData(localName, charactersBuffer.toString());
+        }
+    }
+
+    @Override
+    protected ContentHandler createChildTagHandler(String namespaceURI, String localName,
+                                                   String qName, Attributes attributes) {
+        // try to pass unknown tags to someone else
+        return loaderContext.getFactory().createHandler(namespaceURI, localName, this);
+    }
+
+    public void setTargetNamespace(String targetNamespace) {
+        this.targetNamespace = targetNamespace;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NoopDataChannelMetaData.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NoopDataChannelMetaData.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NoopDataChannelMetaData.java
new file mode 100644
index 0000000..cbcb696
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/NoopDataChannelMetaData.java
@@ -0,0 +1,42 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.configuration.ConfigurationNode;
+
+/**
+ * Noop implementation of {@link DataChannelMetaData}.
+ * Used by Cayenne runtime by default as it doesn't need this information.
+ *
+ * @see DefaultDataChannelMetaData
+ * @since 4.1
+ */
+public class NoopDataChannelMetaData implements DataChannelMetaData {
+
+    @Override
+    public void add(ConfigurationNode key, Object value) {
+        // noop
+    }
+
+    @Override
+    public <T> T get(ConfigurationNode key, Class<T> type) {
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java
new file mode 100644
index 0000000..e9ee9aa
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java
@@ -0,0 +1,210 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.CallbackDescriptor;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class ObjEntityHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String OBJ_ENTITY_TAG = "obj-entity";
+    private static final String OBJ_ATTRIBUTE_TAG = "obj-attribute";
+    private static final String OBJ_ATTRIBUTE_OVERRIDE_TAG = "attribute-override";
+    private static final String EMBEDDED_ATTRIBUTE_TAG = "embedded-attribute";
+    private static final String QUALIFIER_TAG = "qualifier";
+
+    // lifecycle listeners and callbacks related
+    private static final String POST_ADD_TAG = "post-add";
+    private static final String PRE_PERSIST_TAG = "pre-persist";
+    private static final String POST_PERSIST_TAG = "post-persist";
+    private static final String PRE_UPDATE_TAG = "pre-update";
+    private static final String POST_UPDATE_TAG = "post-update";
+    private static final String PRE_REMOVE_TAG = "pre-remove";
+    private static final String POST_REMOVE_TAG = "post-remove";
+    private static final String POST_LOAD_TAG = "post-load";
+
+    private DataMap map;
+
+    private ObjEntity entity;
+
+    private ObjAttribute lastAttribute;
+
+    public ObjEntityHandler(NamespaceAwareNestedTagHandler parentHandler, DataMap map) {
+        super(parentHandler);
+        this.map = map;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        switch (localName) {
+            case OBJ_ENTITY_TAG:
+                createObjEntity(attributes);
+                return true;
+
+            case OBJ_ATTRIBUTE_TAG:
+                createObjAttribute(attributes);
+                return true;
+
+            case OBJ_ATTRIBUTE_OVERRIDE_TAG:
+                processStartAttributeOverride(attributes);
+                return true;
+
+            case QUALIFIER_TAG:
+                return true;
+
+            case POST_ADD_TAG:
+            case PRE_PERSIST_TAG:
+            case POST_PERSIST_TAG:
+            case PRE_UPDATE_TAG:
+            case POST_UPDATE_TAG:
+            case PRE_REMOVE_TAG:
+            case POST_REMOVE_TAG:
+            case POST_LOAD_TAG:
+                createCallback(localName, attributes);
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected ContentHandler createChildTagHandler(String namespaceURI, String localName, String qName, Attributes attributes) {
+        if(namespaceURI.equals(targetNamespace)) {
+            switch (localName) {
+                case EMBEDDED_ATTRIBUTE_TAG:
+                    return new EmbeddableAttributeHandler(this, entity);
+            }
+        }
+
+        return super.createChildTagHandler(namespaceURI, localName, qName, attributes);
+    }
+
+    @Override
+    protected void processCharData(String localName, String data) {
+        switch (localName) {
+            case QUALIFIER_TAG:
+                createQualifier(data);
+                break;
+        }
+    }
+
+    private void createObjEntity(Attributes attributes) {
+        entity = new ObjEntity(attributes.getValue("name"));
+        entity.setClassName(attributes.getValue("className"));
+        entity.setClientClassName(attributes.getValue("clientClassName"));
+        entity.setAbstract(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("abstract")));
+        entity.setReadOnly(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("readOnly")));
+        entity.setServerOnly(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("serverOnly")));
+        if ("optimistic".equals(attributes.getValue("", "lock-type"))) {
+            entity.setDeclaredLockType(ObjEntity.LOCK_TYPE_OPTIMISTIC);
+        }
+
+        String superEntityName = attributes.getValue("superEntityName");
+        if (superEntityName != null) {
+            entity.setSuperEntityName(superEntityName);
+        } else {
+            entity.setSuperClassName(attributes.getValue("superClassName"));
+            entity.setClientSuperClassName(attributes.getValue("clientSuperClassName"));
+        }
+        entity.setDbEntityName(attributes.getValue("dbEntityName"));
+
+        map.addObjEntity(entity);
+    }
+
+    private void createObjAttribute(Attributes attributes) {
+        String dbPath = attributes.getValue("db-attribute-path");
+        if (dbPath == null) {
+            dbPath = attributes.getValue("db-attribute-name");
+        }
+
+        lastAttribute = new ObjAttribute(attributes.getValue("name"));
+        lastAttribute.setType(attributes.getValue("type"));
+        lastAttribute.setUsedForLocking(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("lock")));
+        lastAttribute.setDbAttributePath(dbPath);
+        entity.addAttribute(lastAttribute);
+    }
+
+    private void processStartAttributeOverride(Attributes attributes) {
+        entity.addAttributeOverride(attributes.getValue("name"),
+                attributes.getValue("db-attribute-path"));
+    }
+
+    private CallbackDescriptor getCallbackDescriptor(String type) {
+        if (entity == null) {
+            return null;
+        }
+
+        switch (type) {
+            case POST_ADD_TAG:
+                return entity.getCallbackMap().getPostAdd();
+            case PRE_PERSIST_TAG:
+                return entity.getCallbackMap().getPrePersist();
+            case POST_PERSIST_TAG:
+                return entity.getCallbackMap().getPostPersist();
+            case PRE_UPDATE_TAG:
+                return entity.getCallbackMap().getPreUpdate();
+            case POST_UPDATE_TAG:
+                return entity.getCallbackMap().getPostUpdate();
+            case PRE_REMOVE_TAG:
+                return entity.getCallbackMap().getPreRemove();
+            case POST_REMOVE_TAG:
+                return entity.getCallbackMap().getPostRemove();
+            case POST_LOAD_TAG:
+                return entity.getCallbackMap().getPostLoad();
+        }
+
+        return null;
+    }
+
+    private void createCallback(String type, Attributes attributes) {
+        String methodName = attributes.getValue("method-name");
+        CallbackDescriptor descriptor = getCallbackDescriptor(type);
+        if(descriptor != null) {
+            descriptor.addCallbackMethod(methodName);
+        }
+    }
+
+    private void createQualifier(String qualifier) {
+        if (qualifier.trim().length() == 0) {
+            return;
+        }
+
+        if (entity != null) {
+            entity.setDeclaredQualifier(ExpressionFactory.exp(qualifier));
+        }
+    }
+
+    public ObjEntity getEntity() {
+        return entity;
+    }
+
+    public ObjAttribute getLastAttribute() {
+        return lastAttribute;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjRelationshipHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjRelationshipHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjRelationshipHandler.java
new file mode 100644
index 0000000..63be65d
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjRelationshipHandler.java
@@ -0,0 +1,109 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DeleteRule;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class ObjRelationshipHandler extends NamespaceAwareNestedTagHandler {
+
+    public static final String OBJ_RELATIONSHIP_TAG = "obj-relationship";
+
+    @Deprecated
+    public static final String DB_RELATIONSHIP_REF_TAG = "db-relationship-ref";
+
+    private DataMap map;
+
+    private ObjRelationship objRelationship;
+
+    public ObjRelationshipHandler(NamespaceAwareNestedTagHandler parentHandler, DataMap map) {
+        super(parentHandler);
+        this.map = map;
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        switch (localName) {
+            case OBJ_RELATIONSHIP_TAG:
+                addObjRelationship(attributes);
+                return true;
+
+            case DB_RELATIONSHIP_REF_TAG:
+                addDbRelationshipRef(attributes);
+                return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * <db-relationship-ref> tag deprecated
+     */
+    @Deprecated
+    private void addDbRelationshipRef(Attributes attributes) throws SAXException {
+        String name = attributes.getValue("name");
+        if (name == null) {
+            throw new SAXException("ObjRelationshipHandler::addDbRelationshipRef() - null DbRelationship name for "
+                    + objRelationship.getName());
+        }
+
+        String path = objRelationship.getDbRelationshipPath();
+        objRelationship.setDbRelationshipPath((path != null) ? path + "." + name : name);
+    }
+
+    private void addObjRelationship(Attributes attributes) throws SAXException {
+        String name = attributes.getValue("name");
+        if (null == name) {
+            throw new SAXException("ObjRelationshipHandler::addObjRelationship() - unable to parse target.");
+        }
+
+        String sourceName = attributes.getValue("source");
+        if (sourceName == null) {
+            throw new SAXException("ObjRelationshipHandler::addObjRelationship() - unable to parse source.");
+        }
+
+        ObjEntity source = map.getObjEntity(sourceName);
+        if (source == null) {
+            throw new SAXException("ObjRelationshipHandler::addObjRelationship() - unable to find source " + sourceName);
+        }
+
+        objRelationship = new ObjRelationship(name);
+        objRelationship.setSourceEntity(source);
+        objRelationship.setTargetEntityName(attributes.getValue("target"));
+        objRelationship.setDeleteRule(DeleteRule.deleteRuleForName(attributes.getValue("deleteRule")));
+        objRelationship.setUsedForLocking(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("lock")));
+        objRelationship.setDeferredDbRelationshipPath((attributes.getValue("db-relationship-path")));
+        objRelationship.setCollectionType(attributes.getValue("collection-type"));
+        objRelationship.setMapKey(attributes.getValue("map-key"));
+        source.addRelationship(objRelationship);
+    }
+
+    public ObjRelationship getObjRelationship() {
+        return objRelationship;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ProcedureHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ProcedureHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ProcedureHandler.java
new file mode 100644
index 0000000..72e6a53
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ProcedureHandler.java
@@ -0,0 +1,114 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.Procedure;
+import org.apache.cayenne.map.ProcedureParameter;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class ProcedureHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String PROCEDURE_TAG = "procedure";
+    private static final String PROCEDURE_PARAMETER_TAG = "procedure-parameter";
+
+    private DataMap map;
+
+    private Procedure procedure;
+
+    public ProcedureHandler(NamespaceAwareNestedTagHandler parentHandler, DataMap map) {
+        super(parentHandler);
+        this.map = map;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        switch (localName) {
+            case PROCEDURE_TAG:
+                addProcedure(attributes);
+                return true;
+
+            case PROCEDURE_PARAMETER_TAG:
+                addProcedureParameter(attributes);
+                return true;
+        }
+
+        return false;
+    }
+
+    private void addProcedure(Attributes attributes) throws SAXException{
+        String name = attributes.getValue("name");
+        String returningValue = attributes.getValue("returningValue");
+        if (null == name) {
+            throw new SAXException("ProcedureHandler::addProcedure() - no procedure name.");
+        }
+
+        procedure = new Procedure(name);
+        procedure.setReturningValue(returningValue != null && returningValue.equalsIgnoreCase(DataMapHandler.TRUE));
+        procedure.setSchema(attributes.getValue("schema"));
+        procedure.setCatalog(attributes.getValue("catalog"));
+        map.addProcedure(procedure);
+    }
+
+    private void addProcedureParameter(Attributes attributes) throws SAXException {
+
+        String name = attributes.getValue("name");
+        if (name == null) {
+            throw new SAXException("ProcedureHandler::addProcedureParameter() - no procedure parameter name.");
+        }
+
+        ProcedureParameter parameter = new ProcedureParameter(name);
+
+        String type = attributes.getValue("type");
+        if (type != null) {
+            parameter.setType(TypesMapping.getSqlTypeByName(type));
+        }
+
+        String length = attributes.getValue("length");
+        if (length != null) {
+            parameter.setMaxLength(Integer.parseInt(length));
+        }
+
+        String precision = attributes.getValue("precision");
+        if (precision != null) {
+            parameter.setPrecision(Integer.parseInt(precision));
+        }
+
+        String direction = attributes.getValue("direction");
+        if ("in".equals(direction)) {
+            parameter.setDirection(ProcedureParameter.IN_PARAMETER);
+        } else if ("out".equals(direction)) {
+            parameter.setDirection(ProcedureParameter.OUT_PARAMETER);
+        } else if ("in_out".equals(direction)) {
+            parameter.setDirection(ProcedureParameter.IN_OUT_PARAMETER);
+        }
+
+        procedure.addCallParameter(parameter);
+    }
+
+    public Procedure getProcedure() {
+        return procedure;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/QueryDescriptorHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/QueryDescriptorHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/QueryDescriptorHandler.java
new file mode 100644
index 0000000..4034184
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/QueryDescriptorHandler.java
@@ -0,0 +1,191 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.QueryDescriptor;
+import org.apache.cayenne.map.QueryDescriptorLoader;
+import org.apache.cayenne.util.Util;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class QueryDescriptorHandler extends NamespaceAwareNestedTagHandler {
+
+    private static final String QUERY_DESCRIPTOR_TAG = "query";
+    private static final String QUERY_SQL_TAG = "sql";
+    private static final String QUERY_EJBQL_TAG = "ejbql";
+    private static final String QUERY_QUALIFIER_TAG = "qualifier";
+    private static final String QUERY_ORDERING_TAG = "ordering";
+    private static final String QUERY_PREFETCH_TAG = "prefetch";
+
+    public static final String PROPERTY_TAG = "property";
+
+    private DataMap map;
+
+    private QueryDescriptorLoader queryBuilder;
+    private QueryDescriptor descriptor;
+    private boolean changed;
+
+    private String sqlKey;
+    private String descending;
+    private String ignoreCase;
+
+    public QueryDescriptorHandler(NamespaceAwareNestedTagHandler parentHandler, DataMap map) {
+        super(parentHandler);
+        this.map = map;
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+
+        switch (localName) {
+            case QUERY_DESCRIPTOR_TAG:
+                addQueryDescriptor(attributes);
+                return true;
+
+            case PROPERTY_TAG:
+                addQueryDescriptorProperty(attributes);
+                return true;
+
+            case QUERY_SQL_TAG:
+                this.sqlKey = attributes.getValue("adapter-class");
+                return true;
+
+            case QUERY_ORDERING_TAG:
+                createQueryOrdering(attributes);
+                return true;
+
+            case QUERY_EJBQL_TAG:
+            case QUERY_QUALIFIER_TAG:
+            case QUERY_PREFETCH_TAG:
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void processCharData(String localName, String data) {
+        switch (localName) {
+            case QUERY_SQL_TAG:
+                queryBuilder.addSql(data, sqlKey);
+                break;
+
+            case QUERY_EJBQL_TAG:
+                queryBuilder.setEjbql(data);
+                break;
+
+            case QUERY_QUALIFIER_TAG:
+                createQualifier(data);
+                break;
+
+            case QUERY_ORDERING_TAG:
+                addQueryOrdering(data);
+                break;
+
+            case QUERY_PREFETCH_TAG:
+                queryBuilder.addPrefetch(data);
+                break;
+        }
+    }
+
+    @Override
+    protected void beforeScopeEnd() {
+        map.addQueryDescriptor(getQueryDescriptor());
+    }
+
+    private void addQueryDescriptor(Attributes attributes) throws SAXException {
+        String name = attributes.getValue("name");
+        if (null == name) {
+            throw new SAXException("QueryDescriptorHandler::addQueryDescriptor() - no query name.");
+        }
+
+        queryBuilder = new QueryDescriptorLoader();
+        queryBuilder.setName(name);
+
+        String type = attributes.getValue("type");
+        // Legacy format support (v7 and older)
+        if(type == null) {
+            queryBuilder.setLegacyFactory(attributes.getValue("factory"));
+        } else {
+            queryBuilder.setQueryType(type);
+        }
+
+        String rootName = attributes.getValue("root-name");
+        queryBuilder.setRoot(map, attributes.getValue("root"), rootName);
+
+        // TODO: Andrus, 2/13/2006 'result-type' is only used in ProcedureQuery
+        // and is deprecated in 1.2
+        String resultEntity = attributes.getValue("result-entity");
+        if (!Util.isEmptyString(resultEntity)) {
+            queryBuilder.setResultEntity(resultEntity);
+        }
+
+        changed = true;
+    }
+
+    private void addQueryDescriptorProperty(Attributes attributes) throws SAXException {
+        String name = attributes.getValue("name");
+        if (null == name) {
+            throw new SAXException("QueryDescriptorHandler::addQueryDescriptorProperty() - no property name.");
+        }
+
+        String value = attributes.getValue("value");
+        if (null == value) {
+            throw new SAXException("QueryDescriptorHandler::addQueryDescriptorProperty() - no property value.");
+        }
+
+        queryBuilder.addProperty(name, value);
+        changed = true;
+    }
+
+    private void createQualifier(String qualifier) {
+        if (qualifier.trim().length() == 0) {
+            return;
+        }
+
+        queryBuilder.setQualifier(qualifier);
+        changed = true;
+    }
+
+    private void createQueryOrdering(Attributes attributes) {
+        descending = attributes.getValue("descending");
+        ignoreCase = attributes.getValue("ignore-case");
+    }
+
+    private void addQueryOrdering(String path) {
+        queryBuilder.addOrdering(path, descending, ignoreCase);
+        changed = true;
+    }
+
+    public QueryDescriptor getQueryDescriptor() {
+        if(queryBuilder == null) {
+            return null;
+        }
+        if(descriptor == null || changed) {
+            descriptor = queryBuilder.buildQueryDescriptor();
+            changed = false;
+        }
+        return descriptor;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/RootDataMapHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/RootDataMapHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/RootDataMapHandler.java
new file mode 100644
index 0000000..dba5022
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/RootDataMapHandler.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.map.DataMap;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+/**
+ * @since 4.1
+ */
+public class RootDataMapHandler extends NamespaceAwareNestedTagHandler {
+
+    public RootDataMapHandler(LoaderContext loaderContext) {
+        super(loaderContext);
+        setTargetNamespace(DataMap.SCHEMA_XSD);
+    }
+
+    @Override
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        return false;
+    }
+
+    @Override
+    protected ContentHandler createChildTagHandler(String namespaceURI, String localName, String qName, Attributes attributes) {
+        if(targetNamespace.equals(namespaceURI) && "data-map".equals(localName)) {
+            return new DataMapHandler(this);
+        }
+
+        return super.createChildTagHandler(namespaceURI, localName, qName, attributes);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/SAXNestedTagHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/SAXNestedTagHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/SAXNestedTagHandler.java
new file mode 100644
index 0000000..d2d4e04
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/SAXNestedTagHandler.java
@@ -0,0 +1,175 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * A superclass of nested tag handlers for parsing of XML documents with SAX.
+ * This class is not namespace aware, i.e. tags like &lt;info:property/> and &lt;property/>
+ * will be treated as equal.
+ * Use {@link NamespaceAwareNestedTagHandler} if you need to process namespaces.
+ *
+ * @see NamespaceAwareNestedTagHandler
+ * @since 3.1
+ * @since 4.1 redesigned and moved from {@link org.apache.cayenne.configuration} package
+ */
+public class SAXNestedTagHandler extends DefaultHandler {
+
+    private final static Locator NOOP_LOCATOR = new Locator() {
+
+        public int getColumnNumber() {
+            return -1;
+        }
+
+        public int getLineNumber() {
+            return -1;
+        }
+
+        public String getPublicId() {
+            return "<unknown>";
+        }
+
+        public String getSystemId() {
+            return "<unknown>";
+        }
+    };
+
+    protected LoaderContext loaderContext;
+    protected ContentHandler parentHandler;
+    protected Locator locator;
+
+    public SAXNestedTagHandler(LoaderContext loaderContext) {
+        this.loaderContext = Objects.requireNonNull(loaderContext);
+        this.locator = NOOP_LOCATOR;
+    }
+
+    public SAXNestedTagHandler(SAXNestedTagHandler parentHandler) {
+        this.parentHandler = Objects.requireNonNull(parentHandler);
+        this.loaderContext = Objects.requireNonNull(parentHandler.loaderContext);
+
+        locator = parentHandler.locator;
+        if (locator == null) {
+            locator = NOOP_LOCATOR;
+        }
+    }
+
+    protected String unexpectedTagMessage(String tagFound, String... tagsExpected) {
+
+        List<String> expected = tagsExpected != null
+                ? Arrays.asList(tagsExpected)
+                : Collections.<String> emptyList();
+
+        return String.format("tag <%s> is unexpected at [%d,%d]. The following tags are allowed here: %s",
+                tagFound,
+                locator.getColumnNumber(),
+                locator.getLineNumber(),
+                expected);
+    }
+
+    /**
+     * Main method to process XML content.
+     * Should be override in subclasses, by default do nothing.
+     * Return value should be true if tag was fully processed and shouldn't be passed down to child handler.
+     *
+     * @param namespaceURI namespace for tag
+     * @param localName tag local name (i.e. w/o namespace prefix)
+     * @param attributes tag attributes
+     *
+     * @return true if tag was processed
+     *
+     * @throws SAXException can be thrown to abort parsing
+     *
+     * @see #createChildTagHandler(String, String, String, Attributes)
+     */
+    protected boolean processElement(String namespaceURI, String localName, Attributes attributes) throws SAXException {
+        return true;
+    }
+
+    /**
+     * Callback method that is called before this handler pushed out of parsers stack.
+     * Can be used to flush some aggregate state.
+     */
+    protected void beforeScopeEnd() {
+    }
+
+    /**
+     * This method should be used to create nested handlers to process children elements.
+     * This method should never return {@code null}.
+     *
+     * @param namespaceURI namespace for tag
+     * @param localName tag local name (i.e. w/o namespace prefix)
+     * @param qName tag full name (i.e. with namespace prefix)
+     * @param attributes tag attributes
+     * @return new handler to process child tag
+     */
+    protected ContentHandler createChildTagHandler(String namespaceURI, String localName,
+                                                   String qName, Attributes attributes) {
+        // loose handling of unrecognized tags - just ignore them
+        return new SAXNestedTagHandler(this);
+    }
+
+    protected void stop() {
+        beforeScopeEnd();
+        // pop self from the handler stack
+        loaderContext.getXmlReader().setContentHandler(parentHandler);
+    }
+
+    /**
+     * This method directly called by SAX parser, do not override it directly,
+     * use {@link #processElement(String, String, Attributes)} method instead to process content.
+     *
+     * @see #createChildTagHandler(String, String, String, Attributes)
+     */
+    @Override
+    public void startElement(String namespaceURI, String localName,
+                             String qName, Attributes attributes) throws SAXException {
+        ContentHandler childHandler = createChildTagHandler(namespaceURI, localName, qName, attributes);
+
+        if(!processElement(namespaceURI, localName, attributes)) {
+            childHandler.startElement(namespaceURI, localName, qName, attributes);
+        }
+
+        // push child handler to the stack...
+        loaderContext.getXmlReader().setContentHandler(childHandler);
+    }
+
+    @Override
+    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
+        stop();
+    }
+
+    @Override
+    public void setDocumentLocator(Locator locator) {
+        this.locator = locator;
+    }
+
+    public ContentHandler getParentHandler() {
+        return parentHandler;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataChannelDescriptorLoader.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataChannelDescriptorLoader.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataChannelDescriptorLoader.java
new file mode 100644
index 0000000..e9d104e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataChannelDescriptorLoader.java
@@ -0,0 +1,149 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.ConfigurationException;
+import org.apache.cayenne.configuration.ConfigurationNameMapper;
+import org.apache.cayenne.configuration.ConfigurationTree;
+import org.apache.cayenne.configuration.DataChannelDescriptor;
+import org.apache.cayenne.configuration.DataChannelDescriptorLoader;
+import org.apache.cayenne.configuration.DataMapLoader;
+import org.apache.cayenne.di.AdhocObjectFactory;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.resource.Resource;
+import org.apache.cayenne.util.Util;
+import org.slf4j.LoggerFactory;
+import org.slf4j.Logger;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+
+/**
+ * @since 3.1
+ * @since 4.1 moved from org.apache.cayenne.configuration package
+ */
+public class XMLDataChannelDescriptorLoader implements DataChannelDescriptorLoader {
+
+	private static Logger logger = LoggerFactory.getLogger(XMLDataChannelDescriptorLoader.class);
+
+	static final String CURRENT_PROJECT_VERSION = "10";
+
+	/**
+	 * @deprecated the caller should use password resolving strategy instead of
+	 *             resolving the password on the spot. For one thing this can be
+	 *             used in the Modeler and no password may be available.
+	 */
+	@Deprecated
+	static String passwordFromURL(URL url) {
+		InputStream inputStream;
+		String password = null;
+
+		try {
+			inputStream = url.openStream();
+			password = passwordFromInputStream(inputStream);
+		} catch (IOException exception) {
+			// Log the error while trying to open the stream. A null
+			// password will be returned as a result.
+			logger.warn(exception.getMessage(), exception);
+		}
+
+		return password;
+	}
+
+	/**
+	 * @deprecated the caller should use password resolving strategy instead of
+	 *             resolving the password on the spot. For one thing this can be
+	 *             used in the Modeler and no password may be available.
+	 */
+	@Deprecated
+	static String passwordFromInputStream(InputStream inputStream) {
+		String password = null;
+
+		try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));) {
+
+			password = bufferedReader.readLine();
+		} catch (IOException exception) {
+			logger.warn(exception.getMessage(), exception);
+		} finally {
+
+			try {
+				inputStream.close();
+			} catch (IOException ignored) {
+			}
+		}
+
+		return password;
+	}
+
+	@Inject
+	protected DataMapLoader dataMapLoader;
+
+	@Inject
+	protected ConfigurationNameMapper nameMapper;
+
+	@Inject
+	protected AdhocObjectFactory objectFactory;
+
+	@Inject
+	protected HandlerFactory handlerFactory;
+
+	@Override
+	public ConfigurationTree<DataChannelDescriptor> load(Resource configurationResource) throws ConfigurationException {
+
+		if (configurationResource == null) {
+			throw new NullPointerException("Null configurationResource");
+		}
+
+		URL configurationURL = configurationResource.getURL();
+
+		logger.info("Loading XML configuration resource from " + configurationURL);
+
+		final DataChannelDescriptor descriptor = new DataChannelDescriptor();
+		descriptor.setConfigurationSource(configurationResource);
+		descriptor.setName(nameMapper.configurationNodeName(DataChannelDescriptor.class, configurationResource));
+
+		try(InputStream in = configurationURL.openStream()) {
+			XMLReader parser = Util.createXmlReader();
+			LoaderContext loaderContext = new LoaderContext(parser, handlerFactory);
+			loaderContext.addDataMapListener(new DataMapLoaderListener() {
+				@Override
+				public void onDataMapLoaded(DataMap dataMap) {
+					descriptor.getDataMaps().add(dataMap);
+				}
+			});
+
+			DataChannelHandler rootHandler = new DataChannelHandler(this, descriptor, loaderContext);
+			parser.setContentHandler(rootHandler);
+			parser.setErrorHandler(rootHandler);
+			parser.parse(new InputSource(in));
+		} catch (Exception e) {
+			throw new ConfigurationException("Error loading configuration from %s", e, configurationURL);
+		}
+
+		// TODO: andrus 03/10/2010 - actually provide load failures here...
+		return new ConfigurationTree<>(descriptor, null);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataMapLoader.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataMapLoader.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataMapLoader.java
new file mode 100644
index 0000000..eb82d32
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/XMLDataMapLoader.java
@@ -0,0 +1,102 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.cayenne.configuration.xml;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.configuration.DataMapLoader;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.resource.Resource;
+import org.apache.cayenne.util.Util;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+
+import java.io.InputStream;
+
+/**
+ * @since 3.1
+ * @since 4.1 moved from org.apache.cayenne.configuration package
+ */
+public class XMLDataMapLoader implements DataMapLoader {
+
+    private static final String DATA_MAP_LOCATION_SUFFIX = ".map.xml";
+
+    @Inject
+    protected HandlerFactory handlerFactory;
+
+    private DataMap map;
+
+    public synchronized DataMap load(Resource configurationResource) throws CayenneRuntimeException {
+        try(InputStream in = configurationResource.getURL().openStream()) {
+            XMLReader parser = Util.createXmlReader();
+            LoaderContext loaderContext = new LoaderContext(parser, handlerFactory);
+            loaderContext.addDataMapListener(new DataMapLoaderListener() {
+                @Override
+                public void onDataMapLoaded(DataMap dataMap) {
+                    map = dataMap;
+                }
+            });
+            RootDataMapHandler rootHandler = new RootDataMapHandler(loaderContext);
+
+            parser.setContentHandler(rootHandler);
+            parser.setErrorHandler(rootHandler);
+            parser.parse(new InputSource(in));
+        } catch (Exception e) {
+            throw new CayenneRuntimeException("Error loading configuration from %s", e, configurationResource.getURL());
+        }
+
+        if(map == null) {
+            throw new CayenneRuntimeException("Unable to load data map from %s", configurationResource.getURL());
+        }
+
+        if(map.getName() == null) {
+            // set name based on location if no name provided by map itself
+            map.setName(mapNameFromLocation(configurationResource.getURL().getFile()));
+        }
+        return map;
+    }
+
+    /**
+     * Helper method to guess the map name from its location.
+     */
+    protected String mapNameFromLocation(String location) {
+        if (location == null) {
+            return "Untitled";
+        }
+
+        int lastSlash = location.lastIndexOf('/');
+        if (lastSlash < 0) {
+            lastSlash = location.lastIndexOf('\\');
+        }
+
+        if (lastSlash >= 0 && lastSlash + 1 < location.length()) {
+            location = location.substring(lastSlash + 1);
+        }
+
+        if (location.endsWith(DATA_MAP_LOCATION_SUFFIX)) {
+            location = location.substring(0, location.length() - DATA_MAP_LOCATION_SUFFIX.length());
+        }
+
+        return location;
+    }
+
+    public void setHandlerFactory(HandlerFactory handlerFactory) {
+        this.handlerFactory = handlerFactory;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
index f51c8f0..0647fbc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.conn;
 
 import java.io.Serializable;
 
+import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
 import org.apache.cayenne.configuration.PasswordEncoding;
 import org.apache.cayenne.configuration.PlainTextPasswordEncoder;
 import org.apache.cayenne.di.DIRuntimeException;
@@ -134,58 +135,46 @@ public class DataSourceInfo implements Cloneable, Serializable, XMLSerializable
 	/**
 	 * @since 3.1
 	 */
-	public void encodeAsXML(XMLEncoder encoder) {
-		encoder.println("<data-source>");
-		encoder.indent(1);
-
-		encoder.print("<driver");
-		encoder.printAttribute("value", jdbcDriver);
-		encoder.println("/>");
+	@Override
+	public void encodeAsXML(XMLEncoder encoder, ConfigurationNodeVisitor delegate) {
+		encoder.start("data-source");
 
-		encoder.print("<url");
-		encoder.printAttribute("value", dataSourceUrl);
-		encoder.println("/>");
+		encoder.start("driver").attribute("value", jdbcDriver).end();
+		encoder.start("url").attribute("value", dataSourceUrl).end();
 
-		encoder.print("<connectionPool");
-		encoder.printAttribute("min", String.valueOf(minConnections));
-		encoder.printAttribute("max", String.valueOf(maxConnections));
-		encoder.println("/>");
+		encoder.start("connectionPool")
+				.attribute("min", minConnections)
+				.attribute("max", String.valueOf(maxConnections))
+				.end();
 
-		encoder.print("<login");
-		encoder.printAttribute("userName", userName);
+		encoder.start("login").attribute("userName", userName);
 
 		if (DataSourceInfo.PASSWORD_LOCATION_MODEL.equals(passwordLocation)) {
-
 			PasswordEncoding passwordEncoder = getPasswordEncoder();
-
 			if (passwordEncoder != null) {
 				String passwordEncoded = passwordEncoder.encodePassword(password, passwordEncoderKey);
-				encoder.printAttribute("password", passwordEncoded);
+				encoder.attribute("password", passwordEncoded);
 			}
 		}
 
 		if (!PlainTextPasswordEncoder.class.getName().equals(passwordEncoderClass)) {
-			encoder.printAttribute("encoderClass", passwordEncoderClass);
+			encoder.attribute("encoderClass", passwordEncoderClass);
 		}
 
-		encoder.printAttribute("encoderKey", passwordEncoderKey);
+		encoder.attribute("encoderKey", passwordEncoderKey);
 
 		if (!DataSourceInfo.PASSWORD_LOCATION_MODEL.equals(passwordLocation)) {
-			encoder.printAttribute("passwordLocation", passwordLocation);
+			encoder.attribute("passwordLocation", passwordLocation);
 		}
 
 		// TODO: this is very not nice... we need to clean up the whole
-		// DataSourceInfo
-		// to avoid returning arbitrary labels...
+		// DataSourceInfo to avoid returning arbitrary labels...
 		String passwordSource = getPasswordSource();
 		if (!"Not Applicable".equals(passwordSource)) {
-			encoder.printAttribute("passwordSource", passwordSource);
+			encoder.attribute("passwordSource", passwordSource);
 		}
 
-		encoder.println("/>");
-
-		encoder.indent(-1);
-		encoder.println("</data-source>");
+		encoder.end().end();
 	}
 
 	public DataSourceInfo cloneInfo() {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/c58b6f40/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
index 0d35cea..77a8cdf 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
@@ -31,6 +31,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
 import org.apache.cayenne.exp.parser.ASTScalar;
 import org.apache.cayenne.util.ConversionUtil;
 import org.apache.cayenne.util.HashCodeBuilder;
@@ -679,14 +680,15 @@ public abstract class Expression implements Serializable, XMLSerializable {
 	 * 
 	 * @since 1.1
 	 */
-	public void encodeAsXML(XMLEncoder encoder) {
-		encoder.print("<![CDATA[");
+	@Override
+	public void encodeAsXML(XMLEncoder encoder, ConfigurationNodeVisitor delegate) {
+		StringBuilder sb = new StringBuilder();
 		try {
-			appendAsString(encoder.getPrintWriter());
+			appendAsString(sb);
 		} catch (IOException e) {
 			throw new CayenneRuntimeException("Unexpected IO exception appending to PrintWriter", e);
 		}
-		encoder.print("]]>");
+		encoder.cdata(sb.toString(), true);
 	}
 
 	/**