Merge branch 'metadata' into service-test
diff --git a/dubbo-admin-backend/pom.xml b/dubbo-admin-backend/pom.xml
index 7986fbd..d2510ae 100644
--- a/dubbo-admin-backend/pom.xml
+++ b/dubbo-admin-backend/pom.xml
@@ -92,6 +92,10 @@
             </exclusions>
         </dependency>
         <dependency>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-recipes</artifactId>
+        </dependency>
+        <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>
         </dependency>
diff --git a/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/controller/ServiceTestController.java b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/controller/ServiceTestController.java
new file mode 100644
index 0000000..3ded7c2
--- /dev/null
+++ b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/controller/ServiceTestController.java
@@ -0,0 +1,20 @@
+package org.apache.dubbo.admin.controller;
+
+import org.apache.dubbo.admin.model.dto.ServiceTestDTO;
+import org.apache.dubbo.admin.service.impl.GenericServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/{env}/test")
+public class ServiceTestController {
+
+    @Autowired
+    private GenericServiceImpl genericService;
+
+    @RequestMapping(method = RequestMethod.POST)
+    public Object test(@PathVariable String env, @RequestBody ServiceTestDTO serviceTestDTO) {
+        return genericService.invoke(serviceTestDTO.getService(), serviceTestDTO.getMethod(), serviceTestDTO.getTypes(), null);
+//        return null;
+    }
+}
diff --git a/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/model/dto/ServiceTestDTO.java b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/model/dto/ServiceTestDTO.java
new file mode 100644
index 0000000..7934e7c
--- /dev/null
+++ b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/model/dto/ServiceTestDTO.java
@@ -0,0 +1,40 @@
+package org.apache.dubbo.admin.model.dto;
+
+public class ServiceTestDTO {
+    private String service;
+    private String method;
+    private String[] types;
+    private String params;
+
+    public String getService() {
+        return service;
+    }
+
+    public void setService(String service) {
+        this.service = service;
+    }
+
+    public String getMethod() {
+        return method;
+    }
+
+    public void setMethod(String method) {
+        this.method = method;
+    }
+
+    public String[] getTypes() {
+        return types;
+    }
+
+    public void setTypes(String[] types) {
+        this.types = types;
+    }
+
+    public String getParams() {
+        return params;
+    }
+
+    public void setParams(String params) {
+        this.params = params;
+    }
+}
diff --git a/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/service/impl/GenericServiceImpl.java b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/service/impl/GenericServiceImpl.java
new file mode 100644
index 0000000..6b6e748
--- /dev/null
+++ b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/service/impl/GenericServiceImpl.java
@@ -0,0 +1,42 @@
+package org.apache.dubbo.admin.service.impl;
+
+import org.apache.dubbo.config.ApplicationConfig;
+import org.apache.dubbo.config.ReferenceConfig;
+import org.apache.dubbo.config.RegistryConfig;
+import org.apache.dubbo.registry.Registry;
+import org.apache.dubbo.rpc.service.GenericService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+
+@Component
+public class GenericServiceImpl {
+
+    private ReferenceConfig<GenericService> reference;
+
+    @Autowired
+    private Registry registry;
+
+    @PostConstruct
+    public void init() {
+        reference = new ReferenceConfig<>();
+        reference.setGeneric(true);
+
+        RegistryConfig registryConfig = new RegistryConfig();
+        registryConfig.setAddress(registry.getUrl().getProtocol() + "://" + registry.getUrl().getAddress());
+
+        ApplicationConfig applicationConfig = new ApplicationConfig();
+        applicationConfig.setName("dubbo-admin");
+        applicationConfig.setRegistry(registryConfig);
+
+        reference.setApplication(applicationConfig);
+    }
+
+    public Object invoke(String service, String method, String[] parameterTypes, Object[] params) {
+
+        reference.setInterface(service);
+        GenericService genericService = reference.get();
+        return genericService.$invoke(method, parameterTypes, params);
+    }
+}
diff --git a/dubbo-admin-frontend/package.json b/dubbo-admin-frontend/package.json
index e4a3919..26e89c6 100644
--- a/dubbo-admin-frontend/package.json
+++ b/dubbo-admin-frontend/package.json
@@ -15,6 +15,7 @@
     "brace": "^0.11.1",
     "http-status": "^1.2.0",
     "js-yaml": "^3.12.0",
+    "jsoneditor": "^5.26.2",
     "vue": "^2.5.2",
     "vue-router": "^3.0.1",
     "vuetify": "^1.2.2",
diff --git a/dubbo-admin-frontend/src/components/ServiceTest.vue b/dubbo-admin-frontend/src/components/ServiceTest.vue
new file mode 100644
index 0000000..b1f05b2
--- /dev/null
+++ b/dubbo-admin-frontend/src/components/ServiceTest.vue
@@ -0,0 +1,170 @@
+<!--
+  - 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.
+  -->
+<template>
+  <v-container grid-list-xl fluid>
+    <v-layout row wrap>
+      <v-flex xs12>
+        <search v-model="filter" label="Search by service name" :submit="search"></search>
+      </v-flex>
+      <v-flex xs12>
+        <h3>Methods</h3>
+      </v-flex>
+      <v-flex xs12>
+        <v-data-table :headers="headers" :items="methods" hide-actions class="elevation-1">
+          <template slot="items" slot-scope="props">
+            <td>{{ props.item.name }}</td>
+            <td><v-chip xs v-for="(type, index) in props.item.parameterTypes" :key="index" label>{{ type }}</v-chip></td>
+            <td><v-chip label>{{ props.item.returnType }}</v-chip></td>
+            <td class="text-xs-right">
+              <v-tooltip bottom>
+                <v-icon small
+                        class="mr-2"
+                        color="blue"
+                        slot="activator"
+                        @click="toTest(props.item)">input</v-icon>
+                <span>Try it</span>
+              </v-tooltip>
+            </td>
+          </template>
+        </v-data-table>
+      </v-flex>
+    </v-layout>
+
+    <v-dialog v-model="modal.enable" width="1000px" persistent>
+      <v-card>
+        <v-card-title>
+          <span class="headline">Test {{ modal.method }}</span>
+        </v-card-title>
+        <v-container grid-list-xl fluid>
+          <v-layout row>
+            <v-flex lg6>
+              <json-editor v-model="modal.json" />
+            </v-flex>
+            <v-flex lg6>
+            </v-flex>
+          </v-layout>
+        </v-container>
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn color="darken-1"
+                 flat
+                 @click="modal.enable = false">Close</v-btn>
+          <v-btn color="primary"
+                 depressed
+                 @click="test">Execute</v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+  </v-container>
+</template>
+
+<script>
+  import JsonEditor from '@/components/public/JsonEditor'
+  import Search from '@/components/public/Search'
+
+  export default {
+    name: 'ServiceTest',
+    data () {
+      return {
+        filter: 'org.apache.dubbo.demo.api.DemoService',
+        headers: [
+          {
+            text: 'Method Name',
+            value: 'method',
+            sortable: false
+          },
+          {
+            text: 'Parameter List',
+            value: 'parameter',
+            sortable: false
+          },
+          {
+            text: 'Return Type',
+            value: 'returnType',
+            sortable: false
+          },
+          {
+            text: '',
+            value: 'operation',
+            sortable: false
+          }
+        ],
+        service: null,
+        methods: [],
+        modal: {
+          method: null,
+          enable: false,
+          types: null,
+          json: []
+        }
+      }
+    },
+    methods: {
+      search () {
+        if (this.filter == null) {
+          this.filter = ''
+        }
+        this.$router.push({
+          path: 'test',
+          query: { service: this.filter }
+        })
+        this.$axios.get('/service/' + this.filter).then(response => {
+          this.service = response.data
+          if (this.service.hasOwnProperty('metadata')) {
+            this.methods = this.service.metadata.methods
+          }
+        }).catch(error => {
+          this.showSnackbar('error', error.response.data.message)
+        })
+      },
+      toTest (item) {
+        Object.assign(this.modal, {
+          enable: true,
+          method: item.name
+        })
+        this.modal.json = []
+        this.modal.types = item.parameterTypes
+        item.parameterTypes.forEach((i, index) => {
+          this.modal.json.push(this.getType(i))
+        })
+      },
+      test () {
+        this.$axios.post('/test', {
+          service: this.service.metadata.canonicalName,
+          method: this.modal.method,
+          types: this.modal.types,
+          params: JSON.stringify(this.modal.json)
+        }).then(response => {
+          console.log(response)
+        })
+      },
+      getType (type) {
+        if (type.indexOf('java.util.List') === 0) {
+          return []
+        } else if (type.indexOf('java.util.Map') === 0) {
+          return []
+        } else {
+          return ''
+        }
+      }
+    },
+    components: {
+      JsonEditor,
+      Search
+    }
+  }
+</script>
diff --git a/dubbo-admin-frontend/src/components/public/Footers.vue b/dubbo-admin-frontend/src/components/public/Footers.vue
index a5bbab6..331bea7 100644
--- a/dubbo-admin-frontend/src/components/public/Footers.vue
+++ b/dubbo-admin-frontend/src/components/public/Footers.vue
@@ -16,7 +16,7 @@
   -->
 
 <template>
-  <v-footer inset height="auto" class="pa-3 footer-border-top" app>
+  <v-footer inset height="auto" class="pa-3 footer-border-top">
     <v-spacer></v-spacer>
     <span class="caption mr-1"><strong>Copyright</strong> &copy;{{ new Date().getFullYear() }} <strong>The Apache Software Foundation.</strong></span>
   </v-footer>
diff --git a/dubbo-admin-frontend/src/components/public/JsonEditor.vue b/dubbo-admin-frontend/src/components/public/JsonEditor.vue
new file mode 100644
index 0000000..c4cbbf5
--- /dev/null
+++ b/dubbo-admin-frontend/src/components/public/JsonEditor.vue
@@ -0,0 +1,84 @@
+<!--
+  - 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.
+  -->
+<template>
+  <div class="jsoneditor-vue-container"></div>
+</template>
+
+<script>
+  import JSONEditor from 'jsoneditor'
+  import 'jsoneditor/dist/jsoneditor.css'
+
+  export default {
+    name: 'json-editor',
+    props: {
+      value: Object,
+      mode: {
+        type: String,
+        default: 'tree'
+      },
+      modes: {
+        type: Array,
+        default: () => ['tree', 'code']
+      },
+      templates: Array
+    },
+    data () {
+      return {
+        $jsoneditor: null
+      }
+    },
+    watch: {
+      value (newVal, oldVal) {
+        if (newVal !== oldVal && this.$jsoneditor) {
+          this.$jsoneditor.update(newVal)
+        }
+      }
+    },
+    mounted () {
+      const options = {
+        name: 'Parameters',
+        navigationBar: false,
+        search: false,
+        mode: this.mode,
+        modes: this.modes,
+        onChange: () => {
+          if (this.$jsoneditor) {
+            var json = this.$jsoneditor.get()
+            this.$emit('input', json)
+          }
+        },
+        templates: this.templates
+      }
+      this.$jsoneditor = new JSONEditor(this.$el, options)
+      this.$jsoneditor.set(this.value)
+      this.$jsoneditor.expandAll()
+    },
+    beforeDestroy () {
+      if (this.$jsoneditor) {
+        this.$jsoneditor.destroy()
+        this.$jsoneditor = null
+      }
+    }
+  }
+</script>
+
+<style scoped>
+  .jsoneditor-vue-container {
+    width: 100%;
+    height: 100%;
+  }
+</style>
diff --git a/dubbo-admin-frontend/src/router/index.js b/dubbo-admin-frontend/src/router/index.js
index 38dc6f0..2fe356e 100644
--- a/dubbo-admin-frontend/src/router/index.js
+++ b/dubbo-admin-frontend/src/router/index.js
@@ -19,6 +19,7 @@
 import Router from 'vue-router'
 import ServiceSearch from '@/components/ServiceSearch'
 import ServiceDetail from '@/components/ServiceDetail'
+import ServiceTest from '@/components/ServiceTest'
 import RoutingRule from '@/components/governance/RoutingRule'
 import TagRule from '@/components/governance/TagRule'
 import AccessControl from '@/components/governance/AccessControl'
@@ -41,6 +42,11 @@
       component: ServiceDetail
     },
     {
+      path: '/test',
+      name: 'ServiceTest',
+      component: ServiceTest
+    },
+    {
       path: '/governance/routingRule',
       name: 'RoutingRule',
       component: RoutingRule
diff --git a/pom.xml b/pom.xml
index 44eb93a..abee9bd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -109,6 +109,17 @@
 				<version>${curator-version}</version>
 			</dependency>
 			<dependency>
+				<groupId>org.apache.curator</groupId>
+				<artifactId>curator-recipes</artifactId>
+				<version>${curator-version}</version>
+				<exclusions>
+					<exclusion>
+						<groupId>org.apache.zookeeper</groupId>
+						<artifactId>zookeeper</artifactId>
+					</exclusion>
+				</exclusions>
+			</dependency>
+			<dependency>
 				<groupId>com.alibaba</groupId>
 				<artifactId>fastjson</artifactId>
 				<version>${fastjson-version}</version>