diff --git a/testing/google-cloud-appengine-flex-compat/pom.xml b/testing/google-cloud-appengine-flex-compat/pom.xml
new file mode 100644
index 000000000000..f0cde1d999d8
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-compat/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+ google-cloud-pom
+ com.google.cloud
+ 0.11.2-alpha-SNAPSHOT
+ ../..
+
+
+ 4.0.0
+ war
+ google-cloud-appengine-flex-compat
+
+
+ 1.9.51
+ 1.2.1
+ 9.3.8.v20160314
+ 1.7
+
+
+
+
+ ${project.groupId}
+ google-cloud-managed-test
+ ${project.version}
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.maven.plugin}
+
+
+
+ org.apache.maven.plugins
+ 3.3
+ maven-compiler-plugin
+
+ ${java.source.version}
+ ${java.source.version}
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine.maven.plugin}
+
+ 1
+
+
+
+
+
diff --git a/testing/google-cloud-appengine-flex-compat/src/main/webapp/WEB-INF/appengine-web.xml b/testing/google-cloud-appengine-flex-compat/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000000..666ecfc4bf5f
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-compat/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,18 @@
+
+
+ GCLOUD_PROJECT_ID_HERE
+ flex-compat
+ 1
+ true
+ flex
+
+
+
+
+ 1
+
+
+ 1
+ 4
+
+
diff --git a/testing/google-cloud-appengine-flex-compat/src/main/webapp/WEB-INF/web.xml b/testing/google-cloud-appengine-flex-compat/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..5fae1f07743a
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-compat/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/testing/google-cloud-appengine-flex-compat/src/main/webapp/index.html b/testing/google-cloud-appengine-flex-compat/src/main/webapp/index.html
new file mode 100644
index 000000000000..4cd4135f9823
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-compat/src/main/webapp/index.html
@@ -0,0 +1,13 @@
+
+google-cloud-java managed environments tests executor
+
+Enter fully qualified test class names to execute. One class per line, use '#' for comments.
+
+
+
diff --git a/testing/google-cloud-appengine-flex-custom/pom.xml b/testing/google-cloud-appengine-flex-custom/pom.xml
new file mode 100644
index 000000000000..c0d2b9e0bf32
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-custom/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ google-cloud-pom
+ com.google.cloud
+ 0.11.2-alpha-SNAPSHOT
+ ../..
+
+
+ 4.0.0
+ war
+ google-cloud-appengine-flex-custom
+
+
+ 1.2.1
+ 9.3.8.v20160314
+
+
+
+
+ ${project.groupId}
+ google-cloud-managed-test
+ ${project.version}
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.maven.plugin}
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine.maven.plugin}
+
+ 1
+
+
+
+
+
diff --git a/testing/google-cloud-appengine-flex-custom/src/main/appengine/app.yaml b/testing/google-cloud-appengine-flex-custom/src/main/appengine/app.yaml
new file mode 100644
index 000000000000..de7271d2a175
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-custom/src/main/appengine/app.yaml
@@ -0,0 +1,15 @@
+runtime: custom
+env: flex
+
+handlers:
+- url: /.*
+ script: this field is required, but ignored
+
+service: flex-custom
+
+manual_scaling:
+ instances: 1
+
+resources:
+ cpu: 1
+ memory_gb: 6
diff --git a/testing/google-cloud-appengine-flex-custom/src/main/docker/Dockerfile b/testing/google-cloud-appengine-flex-custom/src/main/docker/Dockerfile
new file mode 100644
index 000000000000..787983266963
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-custom/src/main/docker/Dockerfile
@@ -0,0 +1,2 @@
+FROM gcr.io/google_appengine/jetty
+ADD google-cloud-appengine-flex-custom-0.11.2-alpha-SNAPSHOT.war $JETTY_BASE/webapps/root.war
diff --git a/testing/google-cloud-appengine-flex-custom/src/main/webapp/WEB-INF/web.xml b/testing/google-cloud-appengine-flex-custom/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..5fae1f07743a
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-custom/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/testing/google-cloud-appengine-flex-custom/src/main/webapp/index.html b/testing/google-cloud-appengine-flex-custom/src/main/webapp/index.html
new file mode 100644
index 000000000000..4cd4135f9823
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-custom/src/main/webapp/index.html
@@ -0,0 +1,13 @@
+
+google-cloud-java managed environments tests executor
+
+Enter fully qualified test class names to execute. One class per line, use '#' for comments.
+
+
+
diff --git a/testing/google-cloud-appengine-flex-java/pom.xml b/testing/google-cloud-appengine-flex-java/pom.xml
new file mode 100644
index 000000000000..82c0c79b1725
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-java/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ google-cloud-pom
+ com.google.cloud
+ 0.11.2-alpha-SNAPSHOT
+ ../..
+
+
+ 4.0.0
+ war
+ google-cloud-appengine-flex-java
+
+
+ 1.2.1
+ 9.3.8.v20160314
+
+
+
+
+ ${project.groupId}
+ google-cloud-managed-test
+ ${project.version}
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.maven.plugin}
+
+
+
+ com.google.cloud.tools
+ appengine-maven-plugin
+ ${appengine.maven.plugin}
+
+ 1
+
+
+
+
+
diff --git a/testing/google-cloud-appengine-flex-java/src/main/appengine/app.yaml b/testing/google-cloud-appengine-flex-java/src/main/appengine/app.yaml
new file mode 100644
index 000000000000..ff8ef73aa0a7
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-java/src/main/appengine/app.yaml
@@ -0,0 +1,15 @@
+runtime: java
+env: flex
+
+handlers:
+- url: /.*
+ script: this field is required, but ignored
+
+service: flex-java
+
+manual_scaling:
+ instances: 1
+
+resources:
+ cpu: 1
+ memory_gb: 6
diff --git a/testing/google-cloud-appengine-flex-java/src/main/webapp/WEB-INF/web.xml b/testing/google-cloud-appengine-flex-java/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..5fae1f07743a
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-java/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/testing/google-cloud-appengine-flex-java/src/main/webapp/index.html b/testing/google-cloud-appengine-flex-java/src/main/webapp/index.html
new file mode 100644
index 000000000000..4cd4135f9823
--- /dev/null
+++ b/testing/google-cloud-appengine-flex-java/src/main/webapp/index.html
@@ -0,0 +1,13 @@
+
+google-cloud-java managed environments tests executor
+
+Enter fully qualified test class names to execute. One class per line, use '#' for comments.
+
+
+
diff --git a/testing/google-cloud-appengine-java8/pom.xml b/testing/google-cloud-appengine-java8/pom.xml
new file mode 100644
index 000000000000..0b7fd6687b59
--- /dev/null
+++ b/testing/google-cloud-appengine-java8/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+ google-cloud-pom
+ com.google.cloud
+ 0.11.2-alpha-SNAPSHOT
+ ../..
+
+
+ 4.0.0
+ war
+ google-cloud-appengine-java8
+
+
+ 1.9.51
+ 9.3.8.v20160314
+ 1.8
+
+
+
+
+ ${project.groupId}
+ google-cloud-managed-test
+ ${project.version}
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+
+ org.eclipse.jetty
+ jetty-maven-plugin
+ ${jetty.maven.plugin}
+
+
+
+ org.apache.maven.plugins
+ 3.3
+ maven-compiler-plugin
+
+ ${java.source.version}
+ ${java.source.version}
+
+
+
+ com.google.appengine
+ appengine-maven-plugin
+ ${appengine.sdk.version}
+
+
+
+
diff --git a/testing/google-cloud-appengine-java8/src/main/webapp/WEB-INF/appengine-web.xml b/testing/google-cloud-appengine-java8/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 000000000000..d2eeba3504d2
--- /dev/null
+++ b/testing/google-cloud-appengine-java8/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,12 @@
+
+
+ GCLOUD_PROJECT_ID_HERE
+ std-java8
+ 1
+ java8
+ true
+ B8
+
+ 1
+
+
diff --git a/testing/google-cloud-appengine-java8/src/main/webapp/WEB-INF/web.xml b/testing/google-cloud-appengine-java8/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..5fae1f07743a
--- /dev/null
+++ b/testing/google-cloud-appengine-java8/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/testing/google-cloud-appengine-java8/src/main/webapp/index.html b/testing/google-cloud-appengine-java8/src/main/webapp/index.html
new file mode 100644
index 000000000000..4cd4135f9823
--- /dev/null
+++ b/testing/google-cloud-appengine-java8/src/main/webapp/index.html
@@ -0,0 +1,13 @@
+
+google-cloud-java managed environments tests executor
+
+Enter fully qualified test class names to execute. One class per line, use '#' for comments.
+
+
+
diff --git a/testing/google-cloud-managed-test/pom.xml b/testing/google-cloud-managed-test/pom.xml
new file mode 100644
index 000000000000..a415e3414e51
--- /dev/null
+++ b/testing/google-cloud-managed-test/pom.xml
@@ -0,0 +1,228 @@
+
+
+
+ google-cloud-pom
+ com.google.cloud
+ 0.11.2-alpha-SNAPSHOT
+ ../../pom.xml
+
+ 4.0.0
+
+ google-cloud-managed-test
+
+
+
+ ${project.groupId}
+ google-cloud-bigquery
+ ${beta.version}
+
+
+ ${project.groupId}
+ google-cloud-bigquery
+ ${beta.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-compute
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-compute
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-core
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-core
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-datastore
+ ${beta.version}
+
+
+ ${project.groupId}
+ google-cloud-datastore
+ ${beta.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-dns
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-dns
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-errorreporting
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-errorreporting
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-language
+ ${beta.version}
+
+
+ ${project.groupId}
+ google-cloud-language
+ ${beta.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-logging
+ ${beta.version}
+
+
+ ${project.groupId}
+ google-cloud-logging
+ ${beta.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-monitoring
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-monitoring
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-pubsub
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-pubsub
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-resourcemanager
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-resourcemanager
+ ${project.version}
+ test-jar
+
+
+
+
+ ${project.groupId}
+ google-cloud-speech
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-speech
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-storage
+ ${storage.version}
+
+
+ ${project.groupId}
+ google-cloud-storage
+ ${storage.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-trace
+ ${project.version}
+
+
+ ${project.groupId}
+ google-cloud-trace
+ ${project.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-translate
+ ${beta.version}
+
+
+ ${project.groupId}
+ google-cloud-translate
+ ${beta.version}
+ test-jar
+
+
+
+ ${project.groupId}
+ google-cloud-vision
+ ${beta.version}
+
+
+ ${project.groupId}
+ google-cloud-vision
+ ${beta.version}
+ test-jar
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+ junit
+ junit
+ 4.12
+
+
+
+ org.easymock
+ easymock
+ 3.4
+
+
+
diff --git a/testing/google-cloud-managed-test/src/main/java/com/google/cloud/managed/test/GcjFlexTestServlet.java b/testing/google-cloud-managed-test/src/main/java/com/google/cloud/managed/test/GcjFlexTestServlet.java
new file mode 100644
index 000000000000..8026f812fd7b
--- /dev/null
+++ b/testing/google-cloud-managed-test/src/main/java/com/google/cloud/managed/test/GcjFlexTestServlet.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ *
+ * 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 com.google.cloud.managed.test;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@WebServlet(name = "flex", value = "/test")
+public class GcjFlexTestServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 523885428311420041L;
+
+ private final ThreadPoolExecutor executor =
+ new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
+ private volatile GcjTestRunner testRunner;
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ try {
+ String classNames = req.getParameter("classes");
+ PrintWriter out = resp.getWriter();
+ List> testClasses = loadClasses(classNames.split("[\\r\\n]+"));
+ String output = runTests(testClasses, req, resp);
+ out.append(output);
+ out.close();
+ } catch (Exception e) {
+ throw new ServletException(e);
+ }
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ PrintWriter out = resp.getWriter();
+ out.append(getTestOutput(req, resp));
+ out.close();
+ }
+
+ private List> loadClasses(String... classNames) throws ClassNotFoundException {
+ List> classes = new ArrayList<>();
+ for (String className : classNames) {
+ String cn = className.trim();
+ if (!cn.isEmpty() && !cn.startsWith("#")) {
+ classes.add(Class.forName(cn));
+ }
+ }
+ return classes;
+ }
+
+ private String runTests(
+ List> classes, HttpServletRequest req, HttpServletResponse resp) {
+ synchronized (executor) {
+ resp.setContentType("text/html");
+ if (executor.getActiveCount() > 0) {
+ return "Cannot start new test: the previous test hasn't completed yet."
+ + "The active test progress is at "
+ + req.getRequestURL()
+ + ".";
+ }
+
+ testRunner = new GcjTestRunner(classes);
+ executor.execute(testRunner);
+ return "Test started. Check progress at "
+ + req.getRequestURL()
+ + "";
+ }
+ }
+
+ private String getTestOutput(HttpServletRequest req, HttpServletResponse resp) {
+ synchronized (executor) {
+ if (testRunner != null) {
+ resp.setContentType("text");
+ return testRunner.getOutput();
+ }
+ resp.setContentType("text/html");
+ int urlUriLenDiff = req.getRequestURL().length() - req.getRequestURI().length();
+ String link = req.getRequestURL().substring(0, urlUriLenDiff);
+ return "Test hasn't been started yet. Go to "
+ + link
+ + " to start a new test";
+
+ }
+ }
+
+ @Override
+ public void destroy() {
+ executor.shutdownNow();
+ super.destroy();
+ }
+}
diff --git a/testing/google-cloud-managed-test/src/main/java/com/google/cloud/managed/test/GcjTestRunner.java b/testing/google-cloud-managed-test/src/main/java/com/google/cloud/managed/test/GcjTestRunner.java
new file mode 100644
index 000000000000..ac1210810c0a
--- /dev/null
+++ b/testing/google-cloud-managed-test/src/main/java/com/google/cloud/managed/test/GcjTestRunner.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ *
+ * 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 com.google.cloud.managed.test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.internal.TextListener;
+import org.junit.runner.JUnitCore;
+
+public class GcjTestRunner implements Runnable {
+ private final JUnitCore unit;
+ private final ByteArrayOutputStream resultBytes;
+ private final PrintStream resultStream;
+ private final List> classes;
+
+ public GcjTestRunner(List> classes) {
+ this.unit = new JUnitCore();
+ this.resultBytes = new ByteArrayOutputStream();
+ this.resultStream = new PrintStream(this.resultBytes);
+ this.unit.addListener(new TextListener(this.resultStream));
+ this.classes = new ArrayList<>(classes);
+ }
+
+ @Override
+ public void run() {
+ synchronized (resultStream) {
+ resultBytes.reset();
+ }
+ for (Class> clazz : classes) {
+ resultStream.append("\n").append("Running ").append(clazz.getName()).append("\n\n");
+ unit.run(clazz);
+ }
+
+ resultStream.append("ALL TESTS COMPLETED");
+ }
+
+ public String getOutput() {
+ //works because PrintStream is thread safe synchronizing on "this".
+ synchronized (resultStream) {
+ try {
+ return resultBytes.toString("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+ }
+}
diff --git a/utilities/update_pom_version.sh b/utilities/update_pom_version.sh
index a4e84f136c26..6b5c9563246d 100755
--- a/utilities/update_pom_version.sh
+++ b/utilities/update_pom_version.sh
@@ -15,7 +15,7 @@ CURRENT_VERSION=$(mvn org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate
CURRENT_VERSION_BASE=$(mvn org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version | grep -Ev '(^\[|\w+:)' | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')
# Get list of directories for which pom.xml must be updated
-module_folders=($(find . -maxdepth 2 -type d | sed -E -n "/^\.\/(google-cloud-contrib\/)?google-cloud(-[a-z]+)+$/p") . ./google-cloud)
+module_folders=($(find . -maxdepth 2 -type d | sed -E -n "/^\.\/(google-cloud-contrib\/|testing\/)?google-cloud(-[a-z0-9]+)+$/p") . ./google-cloud)
CURRENT_SNAPSHOT=""
if [ "${CURRENT_VERSION##*-}" == "SNAPSHOT" ]; then
@@ -39,6 +39,10 @@ echo "Changing version from ${CURRENT_VERSION_BASE}-*${CURRENT_SNAPSHOT} to ${NE
for item in ${module_folders[*]}
do
sed -ri "0,/$CURRENT_VERSION_BASE/s/${CURRENT_VERSION_BASE}(-[a-z]+)?[^<]*/${NEW_VERSION_BASE}\1${NEW_SNAPSHOT}/" ${item}/pom.xml
+ if [ -w ${item}/src/main/docker/Dockerfile ]
+ then
+ sed -ri "s/${CURRENT_VERSION_BASE}(-[a-z]+)?(-SNAPSHOT)?/${NEW_VERSION_BASE}\1${NEW_SNAPSHOT}/" ${item}/src/main/docker/Dockerfile
+ fi
done
sed -ri "0,/$CURRENT_VERSION_BASE/s/${CURRENT_VERSION_BASE}(-[a-z]+)?[^<]*/${NEW_VERSION_BASE}\1${NEW_SNAPSHOT}/" pom.xml
sed -ri "0,/$CURRENT_VERSION_BASE/s/${CURRENT_VERSION_BASE}(-[a-z]+)?[^<]*/${NEW_VERSION_BASE}\1${NEW_SNAPSHOT}/" pom.xml