knox-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From lmc...@apache.org
Subject [knox] branch master updated: KNOX-2056 - Adding Service Definitions management into Admin UI (#169)
Date Tue, 29 Oct 2019 14:03:10 GMT
This is an automated email from the ASF dual-hosted git repository.

lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 1bdbdee  KNOX-2056 - Adding Service Definitions management into Admin UI (#169)
1bdbdee is described below

commit 1bdbdee67be13cc3b83605350536ffc246fb722e
Author: Sandor Molnar <smolnar@apache.org>
AuthorDate: Tue Oct 29 15:02:59 2019 +0100

    KNOX-2056 - Adding Service Definitions management into Admin UI (#169)
    
    * KNOX-2056 - Adding Service Definitions management into Admin UI
    
    * KNOX-2056 - TopologyResource should not overwrite any topology stored in TopologyService
---
 gateway-admin-ui/admin-ui/app/app.component.ts     |   4 +-
 gateway-admin-ui/admin-ui/app/app.module.ts        |   8 +-
 .../resource-detail/resource-detail.component.html |   7 +-
 .../admin-ui/app/resource/resource.component.html  |  11 +-
 .../admin-ui/app/resource/resource.component.ts    |  28 +++-
 .../admin-ui/app/resource/resource.service.ts      |  43 ++++--
 gateway-admin-ui/admin-ui/app/resource/resource.ts |   1 +
 .../app/resourcetypes/resourcetypes.service.ts     |   2 +-
 .../rewrite.rule.ts}                               |   8 +-
 .../rewrite.rules.ts}                              |   9 +-
 .../resource.ts => service-definition/service.ts}  |   8 +-
 .../servicedefinition-detail.component.ts          | 144 +++++++++++++++++++++
 .../servicedefinition.service.ts                   | 142 ++++++++++++++++++++
 .../servicedefinition.ts}                          |  11 +-
 gateway-admin-ui/package-lock.json                 |  16 ++-
 gateway-admin-ui/package.json                      |   2 +
 .../org/apache/knox/gateway/GatewayMessages.java   |   9 ++
 .../impl/DefaultServiceDefinitionRegistry.java     |   6 +-
 .../ServiceDefinitionCollectionMarshaller.java     |  36 ++++--
 .../service/admin/ServiceDefinitionsResource.java  |  59 ++++++---
 .../gateway/service/admin/TopologiesResource.java  |  16 ++-
 21 files changed, 494 insertions(+), 76 deletions(-)

diff --git a/gateway-admin-ui/admin-ui/app/app.component.ts b/gateway-admin-ui/admin-ui/app/app.component.ts
index 195c7e4..63fd7e7 100644
--- a/gateway-admin-ui/admin-ui/app/app.component.ts
+++ b/gateway-admin-ui/admin-ui/app/app.component.ts
@@ -16,6 +16,7 @@
  */
 import {Component} from '@angular/core';
 import {TopologyService} from './topology.service';
+import {ServiceDefinitionService} from './service-definition/servicedefinition.service';
 import {ResourceTypesService} from './resourcetypes/resourcetypes.service';
 
 @Component({
@@ -35,11 +36,12 @@ import {ResourceTypesService} from './resourcetypes/resourcetypes.service';
             </div>
         </div>
     `,
-    providers: [TopologyService, ResourceTypesService]
+    providers: [TopologyService, ServiceDefinitionService, ResourceTypesService]
 })
 
 export class AppComponent {
     constructor(private topologyService: TopologyService,
+                private serviceDefinitionService: ServiceDefinitionService,
                 private resourcetypesService: ResourceTypesService) {
     }
 }
diff --git a/gateway-admin-ui/admin-ui/app/app.module.ts b/gateway-admin-ui/admin-ui/app/app.module.ts
index 2521e9f..c33cf20 100644
--- a/gateway-admin-ui/admin-ui/app/app.module.ts
+++ b/gateway-admin-ui/admin-ui/app/app.module.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import {NgModule} from '@angular/core';
+import {DataTableModule} from 'angular2-datatable';
 import {BrowserModule} from '@angular/platform-browser';
 import {HttpClientModule, HttpClientXsrfModule} from '@angular/common/http';
 import {FormsModule} from '@angular/forms';
@@ -23,6 +24,8 @@ import {APP_BASE_HREF} from '@angular/common';
 
 import {AppComponent} from './app.component';
 import {TopologyService} from './topology.service';
+import {ServiceDefinitionService} from './service-definition/servicedefinition.service';
+import {ServiceDefinitionDetailComponent} from './service-definition/servicedefinition-detail.component';
 import {GatewayVersionService} from './gateway-version.service';
 import {GatewayVersionComponent} from './gateway-version.component';
 import {TopologyComponent} from './topology.component';
@@ -51,11 +54,13 @@ import {ProviderConfigWizardComponent} from './provider-config-wizard/provider-c
         FormsModule,
         CustomFormsModule,
         BsModalModule,
-        AceEditorModule
+        AceEditorModule,
+        DataTableModule
     ],
     declarations: [AppComponent,
         TopologyComponent,
         TopologyDetailComponent,
+        ServiceDefinitionDetailComponent,
         GatewayVersionComponent,
         XmlPipe,
         JsonPrettyPipe,
@@ -70,6 +75,7 @@ import {ProviderConfigWizardComponent} from './provider-config-wizard/provider-c
         ProviderConfigWizardComponent
     ],
     providers: [TopologyService,
+        ServiceDefinitionService,
         GatewayVersionService,
         ResourceComponent,
         ResourceTypesService,
diff --git a/gateway-admin-ui/admin-ui/app/resource-detail/resource-detail.component.html b/gateway-admin-ui/admin-ui/app/resource-detail/resource-detail.component.html
index f6bb824..ab554c9 100644
--- a/gateway-admin-ui/admin-ui/app/resource-detail/resource-detail.component.html
+++ b/gateway-admin-ui/admin-ui/app/resource-detail/resource-detail.component.html
@@ -1,4 +1,4 @@
-<div *ngIf="resourceType && resourceType !== 'Topologies'" class="panel panel-default">
+<div *ngIf="resourceType && resourceType !== 'Topologies' && resourceType !== 'Service Definitions'" class="panel panel-default">
     <div class="panel-heading">
         <h4 class="panel-title">
             {{ getTitleSubject() }} Detail <span *ngIf="hasSelectedResource()"
@@ -551,3 +551,8 @@
 <div *ngIf="resourceType === 'Topologies'">
     <app-topology-detail></app-topology-detail>
 </div>
+
+<!-- Service Definition Details -->
+<div *ngIf="resourceType === 'Service Definitions'">
+    <app-servicedefinition-detail></app-servicedefinition-detail>
+</div>
\ No newline at end of file
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.component.html b/gateway-admin-ui/admin-ui/app/resource/resource.component.html
index d106b87..c586ce2 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.component.html
+++ b/gateway-admin-ui/admin-ui/app/resource/resource.component.html
@@ -1,6 +1,6 @@
 <div>
     <div class="table-responsive" style="width:100%; overflow: auto; overflow-y: scroll">
-        <table class="table table-hover">
+        <table class="table table-hover" [mfData]="resources" #mf="mfDataTable" [mfRowsOnPage]="10">
             <thead>
             <tr>
                 <th>
@@ -23,7 +23,7 @@
             </tr>
             </thead>
             <tbody>
-            <tr *ngFor="let resource of resources"
+            <tr *ngFor="let resource of mf.data"
                 [class.selected]="resource === selectedResource"
                 [class.active]="isSelectedResource(resource)"
                 (click)="onSelect(resource)"
@@ -32,6 +32,13 @@
                 <td *ngIf="resourceType === 'Topologies'">{{resource.timestamp | date:'medium'}}</td>
             </tr>
             </tbody>
+		    <tfoot>
+		    <tr>
+		        <td colspan="4">
+		            <mfBootstrapPaginator [rowsOnPageSet]="[5,10,15]"></mfBootstrapPaginator>
+		        </td>
+		    </tr>
+		    </tfoot>
         </table>
     </div>
     <div>
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.component.ts b/gateway-admin-ui/admin-ui/app/resource/resource.component.ts
index 803b76a..ff682a9 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.component.ts
+++ b/gateway-admin-ui/admin-ui/app/resource/resource.component.ts
@@ -20,6 +20,8 @@ import {ResourceService} from './resource.service';
 import {Resource} from './resource';
 import {TopologyService} from '../topology.service';
 import {Topology} from '../topology';
+import {ServiceDefinitionService} from '../service-definition/servicedefinition.service';
+import {ServiceDefinition} from '../service-definition/servicedefinition';
 import {HttpErrorResponse} from '@angular/common/http';
 
 @Component({
@@ -35,7 +37,8 @@ export class ResourceComponent implements OnInit {
 
     constructor(private resourceTypesService: ResourceTypesService,
                 private resourceService: ResourceService,
-                private topologyService: TopologyService) {
+                private topologyService: TopologyService,
+                private serviceDefinitionService: ServiceDefinitionService) {
     }
 
     ngOnInit() {
@@ -55,7 +58,11 @@ export class ResourceComponent implements OnInit {
 
                 let debugMsg = 'ResourceComponent --> Found ' + resources.length + ' ' + resType + ' resources\n';
                 for (let res of resources) {
-                    debugMsg += '  ' + res.name + '\n';
+                    if (res.service) {
+                        debugMsg += '  ' + res.service['role'] + ' (' + res.service['version'] + ')' + '\n';
+                    } else {
+                        debugMsg += '  ' + res.name + '\n';
+                    }
                 }
                 console.debug(debugMsg);
             })
@@ -73,6 +80,13 @@ export class ResourceComponent implements OnInit {
             topology.name = resource.name;
             topology.href = resource.href;
             this.topologyService.selectedTopology(topology);
+        } else if (this.resourceType === 'Service Definitions') {
+           let serviceDefinition = new ServiceDefinition();
+           serviceDefinition.name = resource.name;
+           serviceDefinition.service = resource.service['name'];
+           serviceDefinition.role = resource.service['role'];
+           serviceDefinition.version = resource.service['version'];
+           this.serviceDefinitionService.selectedServiceDefinition(serviceDefinition);
         } else {
             // Otherwise, notify the resource service
             this.resourceService.selectedResource(resource);
@@ -80,7 +94,14 @@ export class ResourceComponent implements OnInit {
     }
 
     isSelectedResource(res: Resource): boolean {
-        return (res && this.selectedResource) ? (res.name === this.selectedResource.name) : false;
+        return (res && this.selectedResource) ? (res.service && this.selectedResource.service ? this.isSelectedServiceDefinition(res)
+            : (res.name === this.selectedResource.name)) : false;
+    }
+
+    isSelectedServiceDefinition(res: Resource): boolean {
+        return res.service['name'] === this.selectedResource.service['name']
+            && res.service['role'] === this.selectedResource.service['role']
+            && res.service['version'] === this.selectedResource.service['version'];
     }
 
     getResourceTypeSingularDisplayName(resType: string): string {
@@ -89,6 +110,7 @@ export class ResourceComponent implements OnInit {
                 return 'Topology';
             }
             case 'Provider Configurations':
+            case 'Service Definitions':
             case 'Descriptors': {
                 return resType.substring(0, resType.length - 1);
             }
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.service.ts b/gateway-admin-ui/admin-ui/app/resource/resource.service.ts
index a8fcc28..4f88bc5 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.service.ts
+++ b/gateway-admin-ui/admin-ui/app/resource/resource.service.ts
@@ -31,6 +31,7 @@ export class ResourceService {
     descriptorsUrl = this.apiUrl + 'descriptors';
     topologiesUrl = this.apiUrl + 'topologies';
     serviceDiscoveriesUrl = this.apiUrl + 'servicediscoveries';
+    serviceDefinitionsUrl = this.apiUrl + 'servicedefinitions';
 
     selectedResourceTypeSource = new Subject<string>();
     selectedResourceType$ = this.selectedResourceTypeSource.asObservable();
@@ -44,7 +45,6 @@ export class ResourceService {
     changedProviderConfigurationSource = new Subject<Array<ProviderConfig>>();
     changedProviderConfiguration$ = this.changedProviderConfigurationSource.asObservable();
 
-
     constructor(private http: HttpClient) {
         this.initSupportedDiscoveryTypes();
     }
@@ -78,6 +78,9 @@ export class ResourceService {
             case 'Descriptors': {
                 return this.getDescriptorResources();
             }
+            case 'Service Definitions': {
+                return this.getServiceDefinitionResources();
+            }
             case 'Topologies': {
                 return this.getTopologyResources();
             }
@@ -129,11 +132,32 @@ export class ResourceService {
             });
     }
 
+    getServiceDefinitionResources(): Promise<Resource[]> {
+        let headers = this.addJsonHeaders(new HttpHeaders());
+        return this.http.get(this.serviceDefinitionsUrl + '?serviceOnly=true', {headers: headers})
+            .toPromise()
+            .then(response => response['serviceDefinitions'].serviceDefinition as Resource[])
+            .catch((err: HttpErrorResponse) => {
+                console.debug('ResourceService --> getServiceDefinitionResources() --> error: HTTP ' + err.status + ' ' + err.message);
+                if (err.status === 401) {
+                    window.location.assign(document.location.pathname);
+                } else {
+                    return this.handleError(err);
+                }
+            });
+    }
+
     getResource(resType: string, res: Resource): Promise<string> {
         if (res) {
+            let href = res.href;
+            if (resType === 'Service Definitions') {
+                href = this.serviceDefinitionsUrl + '/' + res.service['name'] + '/' + res.service['role'] + '/' + res.service['version']
+                    + '?serviceOnly=true';
+            }
             let headers = new HttpHeaders();
-            headers = (resType === 'Topologies') ? this.addXmlHeaders(headers) : this.addHeaders(headers, res.name);
-            return this.http.get(res.href, {headers: headers, responseType: 'text'})
+            headers = (resType === 'Topologies' || resType === 'Service Definitions') ? this.addXmlHeaders(headers)
+                : this.addHeaders(headers, res.name);
+            return this.http.get(href, {headers: headers, responseType: 'text'})
                 .toPromise()
                 .then(response => {
                     console.debug('ResourceService --> Loading resource ' + res.name + ' :\n' + response);
@@ -343,12 +367,15 @@ export class ResourceService {
     }
 
     public getResourceDisplayName(res: Resource): string {
-        if (res && res.name) {
-            let index = res.name.lastIndexOf('.');
-            return index > 0 ? res.name.substring(0, index) : res.name;
-        } else {
-            return null;
+        if (res) {
+            if (res.service) {
+                return res.service['role'] + ' (' + res.service['version'] + ')';
+            } else if (res.name) {
+                let index = res.name.lastIndexOf('.');
+                return index > 0 ? res.name.substring(0, index) : res.name;
+            }
         }
+        return null;
     }
 
     private handleError(error: any): Promise<any> {
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.ts b/gateway-admin-ui/admin-ui/app/resource/resource.ts
index cb5ba6f..1cdc2e8 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.ts
+++ b/gateway-admin-ui/admin-ui/app/resource/resource.ts
@@ -21,4 +21,5 @@ export class Resource {
     uri: string;
     href: string;
     content: string;
+    service: string;
 }
diff --git a/gateway-admin-ui/admin-ui/app/resourcetypes/resourcetypes.service.ts b/gateway-admin-ui/admin-ui/app/resourcetypes/resourcetypes.service.ts
index 71382c8..cc2dc4f 100644
--- a/gateway-admin-ui/admin-ui/app/resourcetypes/resourcetypes.service.ts
+++ b/gateway-admin-ui/admin-ui/app/resourcetypes/resourcetypes.service.ts
@@ -20,7 +20,7 @@ import {Subject} from 'rxjs/Subject';
 @Injectable()
 export class ResourceTypesService {
 
-    resourceTypes = ['Provider Configurations', 'Descriptors', 'Topologies'];
+    resourceTypes = ['Provider Configurations', 'Descriptors', 'Topologies', 'Service Definitions'];
     selectedResourceTypeSource = new Subject<string>();
     public selectedResourceType$ = this.selectedResourceTypeSource.asObservable();
 
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.ts b/gateway-admin-ui/admin-ui/app/service-definition/rewrite.rule.ts
similarity index 88%
copy from gateway-admin-ui/admin-ui/app/resource/resource.ts
copy to gateway-admin-ui/admin-ui/app/service-definition/rewrite.rule.ts
index cb5ba6f..2f1e701 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.ts
+++ b/gateway-admin-ui/admin-ui/app/service-definition/rewrite.rule.ts
@@ -15,10 +15,8 @@
  * limitations under the License.
  */
 
-export class Resource {
-    timestamp: number;
+export class RewriteRule {
+    dir: string;
     name: string;
-    uri: string;
-    href: string;
-    content: string;
+    pattern: string;
 }
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.ts b/gateway-admin-ui/admin-ui/app/service-definition/rewrite.rules.ts
similarity index 86%
copy from gateway-admin-ui/admin-ui/app/resource/resource.ts
copy to gateway-admin-ui/admin-ui/app/service-definition/rewrite.rules.ts
index cb5ba6f..d662981 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.ts
+++ b/gateway-admin-ui/admin-ui/app/service-definition/rewrite.rules.ts
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {RewriteRule} from './rewrite.rule';
 
-export class Resource {
-    timestamp: number;
-    name: string;
-    uri: string;
-    href: string;
-    content: string;
+export class RewriteRules {
+    rules: RewriteRule[];
 }
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.ts b/gateway-admin-ui/admin-ui/app/service-definition/service.ts
similarity index 88%
copy from gateway-admin-ui/admin-ui/app/resource/resource.ts
copy to gateway-admin-ui/admin-ui/app/service-definition/service.ts
index cb5ba6f..6820ac3 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.ts
+++ b/gateway-admin-ui/admin-ui/app/service-definition/service.ts
@@ -15,10 +15,8 @@
  * limitations under the License.
  */
 
-export class Resource {
-    timestamp: number;
+export class Service {
     name: string;
-    uri: string;
-    href: string;
-    content: string;
+    role: string;
+    version: string;
 }
diff --git a/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition-detail.component.ts b/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition-detail.component.ts
new file mode 100644
index 0000000..02e4461
--- /dev/null
+++ b/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition-detail.component.ts
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {BsModalComponent} from 'ng2-bs3-modal/ng2-bs3-modal';
+import {ServiceDefinition} from './servicedefinition';
+import {ServiceDefinitionService} from './servicedefinition.service';
+import {ResourceTypesService} from '../resourcetypes/resourcetypes.service';
+
+import 'brace/theme/monokai';
+import 'brace/mode/xml';
+
+@Component({
+    selector: 'app-servicedefinition-detail',
+    template: `
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <h4 class="panel-title">{{title}} <span class="pull-right">{{titleSuffix}}</span></h4>
+            </div>
+            <div *ngIf="serviceDefinitionContent" class="panel-body">
+                <ace-editor
+                        [(text)]="serviceDefinitionContent"
+                        [mode]="'xml'"
+                        [options]="options"
+                        [theme]="theme"
+                        style="min-height: 430px; width:100%; overflow: auto;"
+                        (textChanged)="onChange($event)">
+                </ace-editor>
+		        <div class="panel-footer">
+		            <button id="deleteServiceDefinition" (click)="deleteServiceDefConfirmModal.open('sm')"
+		                    class="btn btn-default btn-sm" type="submit">
+		                <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+		            </button>
+		            <button id="updateServiceDefinition" (click)="updateServiceDefConfirmModal.open('sm')"
+		                class="btn btn-default btn-sm pull-right" type="submit">
+		                <span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>
+		            </button>
+		        </div>
+            </div>
+        </div>
+        <bs-modal (onClose)="updateServiceDefinition()" #updateServiceDefConfirmModal>
+            <bs-modal-header [showDismiss]="true">
+                <h4 class="modal-title">Updating Service Definition {{titleSuffix}}</h4>
+            </bs-modal-header>
+            <bs-modal-body>
+                Are you sure you want to update this service definition?
+            </bs-modal-body>
+            <bs-modal-footer>
+                <button type="button" class="btn btn-default btn-sm" data-dismiss="updateServiceDefConfirmModal"
+                        (click)="updateServiceDefConfirmModal.dismiss()">Cancel
+                </button>
+                <button type="button" class="btn btn-primary btn-sm" (click)="updateServiceDefConfirmModal.close()">Ok</button>
+            </bs-modal-footer>
+        </bs-modal>
+        <bs-modal (onClose)="deleteServiceDefinition()" #deleteServiceDefConfirmModal>
+            <bs-modal-header [showDismiss]="true">
+                <h4 class="modal-title">Deleting Service Definition {{titleSuffix}}</h4>
+            </bs-modal-header>
+            <bs-modal-body>
+                Are you sure you want to delete this service definition?
+            </bs-modal-body>
+            <bs-modal-footer>
+                <button type="button" class="btn btn-default btn-sm" data-dismiss="deleteServiceDefConfirmModal"
+                        (click)="deleteServiceDefConfirmModal.dismiss()">Cancel
+                </button>
+                <button type="button" class="btn btn-primary btn-sm" (click)="deleteServiceDefConfirmModal.close()">Ok</button>
+            </bs-modal-footer>
+        </bs-modal>
+    `
+})
+
+export class ServiceDefinitionDetailComponent implements OnInit {
+    serviceDefinition: ServiceDefinition;
+    title = 'Service Definition Detail';
+    titleSuffix: string;
+    serviceDefinitionContent: string;
+    changedServiceDefinitionContent: string;
+    theme: String = 'monokai';
+    options: any = {useWorker: false, printMargin: false};
+
+    @ViewChild('editor') editor;
+
+    @ViewChild('deleteServiceDefConfirmModal')
+    deleteServiceDefConfirmModal: BsModalComponent;
+
+    constructor(private serviceDefinitionService: ServiceDefinitionService, private resourceTypesService: ResourceTypesService) {
+    }
+
+    ngOnInit(): void {
+        this.serviceDefinitionService.selectedServiceDefinition$.subscribe(value => this.populateContent(value));
+    }
+
+    setTitleSuffix(value: string) {
+        this.titleSuffix = value;
+    }
+
+    onChange(code: any) {
+        this.changedServiceDefinitionContent = code;
+    }
+
+    populateContent(serviceDefinition: ServiceDefinition) {
+        if (serviceDefinition) {
+            this.serviceDefinition = serviceDefinition;
+            this.setTitleSuffix(serviceDefinition.role + ' (' + serviceDefinition.version + ')');
+            this.serviceDefinitionService.getServiceDefinitionXml(serviceDefinition)
+                .then(content => this.serviceDefinitionContent = content);
+        }
+    }
+
+    updateServiceDefinition() {
+        this.serviceDefinitionService.updateServiceDefinition(this.changedServiceDefinitionContent ? this.changedServiceDefinitionContent
+                                                                : this.serviceDefinitionContent)
+            .then(() => {
+                // This refreshes the list of service definitions
+                this.resourceTypesService.selectResourceType('Service Definitions');
+            });
+    }
+
+    deleteServiceDefinition() {
+        this.serviceDefinitionService.deleteServiceDefinition(this.serviceDefinition)
+            .then(() => {
+                // This refreshes the list of service definitions
+                this.resourceTypesService.selectResourceType('Service Definitions');
+                // This refreshes the service definition content panel to the first one in the list
+                this.serviceDefinitionService.getServiceDefinitions()
+                    .then(serviceDefinitions => {
+                        this.serviceDefinitionService.selectedServiceDefinition(serviceDefinitions[0]);
+                    });
+            });
+    }
+}
diff --git a/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition.service.ts b/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition.service.ts
new file mode 100644
index 0000000..75e6b6c
--- /dev/null
+++ b/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition.service.ts
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+import {Injectable} from '@angular/core';
+import {HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
+import {RestURLBuilder} from 'rest-url-builder';
+
+import 'rxjs/add/operator/toPromise';
+import {Subject} from 'rxjs/Subject';
+
+import {ServiceDefinition} from './servicedefinition';
+
+ @Injectable()
+export class ServiceDefinitionService {
+    apiUrl = window.location.pathname.replace(new RegExp('admin-ui/.*'), 'api/v1/');
+    serviceDefinitionsBaseUrl = this.apiUrl + 'servicedefinitions';
+    serviceDefinitionUrlTemplate = this.serviceDefinitionsBaseUrl + '/:service/:role/:version';
+    selectedServiceDefinitionSource = new Subject<ServiceDefinition>();
+    selectedServiceDefinition$ = this.selectedServiceDefinitionSource.asObservable();
+    urlBuilder = new RestURLBuilder();
+
+    constructor(private http: HttpClient) {
+    }
+
+    getServiceDefinitions(): Promise<ServiceDefinition[]> {
+        let headers = new HttpHeaders();
+        headers = this.addXmlHeaders(headers);
+        return this.http.get(this.serviceDefinitionsBaseUrl, { headers: headers})
+            .toPromise()
+            .then(response => response['serviceDefinitions'].serviceDefinition as ServiceDefinition[])
+            .catch((err: HttpErrorResponse) => {
+                console.debug('ServiceDefinitionService --> getServiceDefinitions() --> ' + this.serviceDefinitionsBaseUrl
+                              + '\n  error: ' + err.message);
+                if (err.status === 401) {
+                    window.location.assign(document.location.pathname);
+                } else {
+                    return this.handleError(err);
+                }
+            });
+    }
+
+    getServiceDefinitionXml(serviceDefinition: ServiceDefinition) {
+        let headers = new HttpHeaders();
+        headers = this.addXmlHeaders(headers);
+        let urlBuilder = this.urlBuilder.buildRestURL(this.serviceDefinitionUrlTemplate);
+        urlBuilder.setNamedParameter('service', serviceDefinition.service);
+        urlBuilder.setNamedParameter('role', serviceDefinition.role);
+        urlBuilder.setNamedParameter('version', serviceDefinition.version);
+        return this.http.get(urlBuilder.get(), {headers: headers, responseType: 'text'})
+            .toPromise()
+            .then(response => response)
+            .catch((err: HttpErrorResponse) => {
+                console.debug('ServiceDefinitionService --> getServiceDefinitionXml() --> ' + urlBuilder.get()
+                              + '\n  error: ' + err.message);
+                if (err.status === 401) {
+                    window.location.assign(document.location.pathname);
+                } else {
+                    return this.handleError(err);
+                }
+            });
+    }
+
+    updateServiceDefinition(xml: string): Promise<string> {
+        let xheaders = new HttpHeaders();
+        xheaders = this.addXmlHeaders(xheaders);
+        let serviceDefintionXml = xml.replace('<serviceDefinitions>', '').replace('</serviceDefinitions>', '');
+        return this.http.put(this.serviceDefinitionsBaseUrl, serviceDefintionXml, {headers: xheaders, responseType: 'text'})
+            .toPromise()
+            .then(response => response)
+            .catch((err: HttpErrorResponse) => {
+                console.debug('ServiceDefinitionService --> updateServiceDefinition() --> ' + this.serviceDefinitionsBaseUrl
+                              + '\n  error: ' + err.status + ' ' + err.message);
+                if (err.status === 401) {
+                    window.location.assign(document.location.pathname);
+                } else {
+                    return this.handleError(err);
+                }
+            });
+    }
+
+    deleteServiceDefinition(serviceDefinition: ServiceDefinition): Promise<string> {
+        let xheaders = new HttpHeaders();
+        xheaders = this.addXmlHeaders(xheaders);
+        let urlBuilder = this.urlBuilder.buildRestURL(this.serviceDefinitionUrlTemplate);
+        urlBuilder.setNamedParameter('service', serviceDefinition.service);
+        urlBuilder.setNamedParameter('role', serviceDefinition.role);
+        urlBuilder.setNamedParameter('version', serviceDefinition.version);
+        return this.http.delete(urlBuilder.get(), {headers: xheaders, responseType: 'text'})
+            .toPromise()
+            .then(response => response)
+            .catch((err: HttpErrorResponse) => {
+                console.debug('ServiceDefinitionService --> deleteServiceDefinition() --> ' + urlBuilder.get()
+                              + '\n  error: ' + err.status + ' ' + err.message);
+                if (err.status === 401) {
+                    window.location.assign(document.location.pathname);
+                } else {
+                    return this.handleError(err);
+                }
+            });
+    }
+
+    selectedServiceDefinition(value: ServiceDefinition) {
+        this.selectedServiceDefinitionSource.next(value);
+    }
+
+    addJsonHeaders(headers: HttpHeaders): HttpHeaders {
+        return this.addCsrfHeaders(headers.append('Accept', 'application/json')
+            .append('Content-Type', 'application/json'));
+    }
+
+    addXmlHeaders(headers: HttpHeaders): HttpHeaders {
+        return this.addCsrfHeaders(headers.append('Accept', 'application/xml')
+            .append('Content-Type', 'application/xml'));
+    }
+
+    addCsrfHeaders(headers: HttpHeaders): HttpHeaders {
+        return this.addXHRHeaders(headers.append('X-XSRF-Header', 'admin-ui'));
+    }
+
+    addXHRHeaders(headers: HttpHeaders): HttpHeaders {
+        return headers.append('X-Requested-With', 'XMLHttpRequest');
+    }
+
+    private handleError(error: any): Promise<any> {
+        console.error('An error occurred', error); // for demo purposes only
+        return Promise.reject(error.message || error);
+    }
+
+}
diff --git a/gateway-admin-ui/admin-ui/app/resource/resource.ts b/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition.ts
similarity index 82%
copy from gateway-admin-ui/admin-ui/app/resource/resource.ts
copy to gateway-admin-ui/admin-ui/app/service-definition/servicedefinition.ts
index cb5ba6f..8a71e6a 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.ts
+++ b/gateway-admin-ui/admin-ui/app/service-definition/servicedefinition.ts
@@ -14,11 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {RewriteRules} from './rewrite.rules';
+import {Service} from './service';
 
-export class Resource {
-    timestamp: number;
+export class ServiceDefinition {
     name: string;
-    uri: string;
-    href: string;
-    content: string;
+    service: string;
+    role: string;
+    version: string;
 }
diff --git a/gateway-admin-ui/package-lock.json b/gateway-admin-ui/package-lock.json
index 15a67b8..584b8a7 100644
--- a/gateway-admin-ui/package-lock.json
+++ b/gateway-admin-ui/package-lock.json
@@ -379,6 +379,14 @@
       "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
       "dev": true
     },
+    "angular2-datatable": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/angular2-datatable/-/angular2-datatable-0.6.0.tgz",
+      "integrity": "sha1-ygCPdAh/DLh9pXCe0vLR0GF3JjI=",
+      "requires": {
+        "lodash": "^4.0.0"
+      }
+    },
     "ansi-html": {
       "version": "0.0.7",
       "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
@@ -4683,8 +4691,7 @@
     "lodash": {
       "version": "4.17.11",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
-      "dev": true
+      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
     },
     "lodash.assign": {
       "version": "4.2.0",
@@ -6967,6 +6974,11 @@
       "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
       "dev": true
     },
+    "rest-url-builder": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/rest-url-builder/-/rest-url-builder-1.0.6.tgz",
+      "integrity": "sha1-TmYdivTydMX/Li/EnfKjcS65NiI="
+    },
     "ret": {
       "version": "0.1.15",
       "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
diff --git a/gateway-admin-ui/package.json b/gateway-admin-ui/package.json
index ab9e5ea..ee6309f 100644
--- a/gateway-admin-ui/package.json
+++ b/gateway-admin-ui/package.json
@@ -18,6 +18,7 @@
     "@angular/platform-browser": "^5.2.0",
     "@angular/platform-browser-dynamic": "^5.2.0",
     "@angular/router": "^5.2.0",
+    "angular2-datatable": "^0.6.0",
     "bootstrap": "^3.4.1",
     "core-js": "^2.6.4",
     "jquery": ">=3.0.0",
@@ -26,6 +27,7 @@
     "ng2-bs3-modal": "^0.13.0",
     "ng2-validation": "^4.2.0",
     "popper.js": "^1.14.7",
+    "rest-url-builder": "^1.0.6",
     "rxjs": "^5.5.2",
     "ts-helpers": "^1.1.1",
     "zone.js": "^0.8.29"
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
index 430ecc2..0a3781f 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
@@ -670,4 +670,13 @@ public interface GatewayMessages {
 
   @Message(level = MessageLevel.INFO, text = "Redeploying topology {0} due to service definition change {1} / {2} / {3}")
   void redeployingTopologyOnServiceDefinitionChange(String topologyName, String serviceName, String role, String version);
+
+  @Message(level = MessageLevel.INFO, text = "Saved service definition {0} / {1} / {2}")
+  void savedServiceDefinitionChange(String serviceName, String role, String version);
+
+  @Message(level = MessageLevel.INFO, text = "Updated service definition {0} / {1} / {2}")
+  void updatedServiceDefinitionChange(String serviceName, String role, String version);
+
+  @Message(level = MessageLevel.INFO, text = "Deleted service definition {0} / {1} / {2}")
+  void deletedServiceDefinitionChange(String serviceName, String role, String version);
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/registry/impl/DefaultServiceDefinitionRegistry.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/registry/impl/DefaultServiceDefinitionRegistry.java
index 4c93fd8..41030c2 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/registry/impl/DefaultServiceDefinitionRegistry.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/registry/impl/DefaultServiceDefinitionRegistry.java
@@ -33,6 +33,7 @@ import java.nio.file.Paths;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -148,7 +149,7 @@ public class DefaultServiceDefinitionRegistry implements ServiceDefinitionRegist
   public Set<ServiceDefinitionPair> getServiceDefinitions() {
     readLock.lock();
     try {
-      return serviceDefinitions;
+      return Collections.unmodifiableSet(serviceDefinitions);
     } finally {
       readLock.unlock();
     }
@@ -157,11 +158,13 @@ public class DefaultServiceDefinitionRegistry implements ServiceDefinitionRegist
   @Override
   public void saveServiceDefinition(ServiceDefinitionPair serviceDefinition) throws ServiceDefinitionRegistryException {
     saveOrUpdateServiceDefinition(serviceDefinition, false);
+    LOG.savedServiceDefinitionChange(serviceDefinition.getService().getName(), serviceDefinition.getService().getRole(), serviceDefinition.getService().getVersion());
   }
 
   @Override
   public void saveOrUpdateServiceDefinition(ServiceDefinitionPair serviceDefinition) throws ServiceDefinitionRegistryException {
     saveOrUpdateServiceDefinition(serviceDefinition, true);
+    LOG.updatedServiceDefinitionChange(serviceDefinition.getService().getName(), serviceDefinition.getService().getRole(), serviceDefinition.getService().getVersion());
   }
 
   public void saveOrUpdateServiceDefinition(ServiceDefinitionPair serviceDefinition, boolean allowUpdate) throws ServiceDefinitionRegistryException {
@@ -228,6 +231,7 @@ public class DefaultServiceDefinitionRegistry implements ServiceDefinitionRegist
         writeLock.unlock();
       }
       notifyListeners(name, role, version);
+      LOG.deletedServiceDefinitionChange(name, role, version);
     } else {
       throw new ServiceDefinitionRegistryException("There is no service definition with the given attributes: " + name + "," + role + "," + version);
     }
diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionCollectionMarshaller.java b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionCollectionMarshaller.java
index 95f4307..2bc8477 100644
--- a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionCollectionMarshaller.java
+++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionCollectionMarshaller.java
@@ -21,6 +21,8 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.ws.rs.Produces;
 import javax.ws.rs.WebApplicationException;
@@ -28,17 +30,19 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.ext.MessageBodyWriter;
 import javax.ws.rs.ext.Provider;
-import javax.xml.bind.JAXBContext;
 import javax.xml.bind.JAXBException;
 import javax.xml.bind.Marshaller;
 
 import org.apache.knox.gateway.service.admin.ServiceDefinitionsResource.ServiceDefinitionsWrapper;
+import org.eclipse.persistence.jaxb.JAXBContextFactory;
+import org.eclipse.persistence.jaxb.JAXBContextProperties;
 
 @Provider
 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
 public class ServiceDefinitionCollectionMarshaller implements MessageBodyWriter<ServiceDefinitionsResource.ServiceDefinitionsWrapper> {
 
-  private static Marshaller marshaller;
+  private static Marshaller xmlMarshaller;
+  private static Marshaller jsonMarshaller;
 
   @Override
   public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
@@ -54,17 +58,33 @@ public class ServiceDefinitionCollectionMarshaller implements MessageBodyWriter<
   public void writeTo(ServiceDefinitionsWrapper instance, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
       MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
     try {
-      getMarshaller().marshal(instance, entityStream);
+      getMarshaller(mediaType).marshal(instance, entityStream);
     } catch (JAXBException e) {
       throw new IOException(e);
     }
   }
 
-  private synchronized Marshaller getMarshaller() throws JAXBException {
-    if (marshaller == null) {
-      marshaller = JAXBContext.newInstance(ServiceDefinitionsWrapper.class).createMarshaller();
-      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+  private Marshaller getMarshaller(MediaType mediaType) throws JAXBException {
+    return MediaType.APPLICATION_JSON_TYPE.getSubtype().equals(mediaType.getSubtype()) ? getJsonMarshaller() : getXmlMarshaller();
+  }
+
+  private synchronized Marshaller getXmlMarshaller() throws JAXBException {
+    if (xmlMarshaller == null) {
+      final Map<String, Object> properties = new HashMap<>(1);
+      properties.put(JAXBContextProperties.MEDIA_TYPE, MediaType.APPLICATION_XML);
+      xmlMarshaller = JAXBContextFactory.createContext(new Class[] { ServiceDefinitionsWrapper.class }, properties).createMarshaller();
+      xmlMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+    }
+    return xmlMarshaller;
+  }
+
+  private synchronized Marshaller getJsonMarshaller() throws JAXBException {
+    if (jsonMarshaller == null) {
+      final Map<String, Object> properties = new HashMap<>(1);
+      properties.put(JAXBContextProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);
+      jsonMarshaller = JAXBContextFactory.createContext(new Class[] { ServiceDefinitionsWrapper.class }, properties).createMarshaller();
+      jsonMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
     }
-    return marshaller;
+    return jsonMarshaller;
   }
 }
diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionsResource.java b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionsResource.java
index 25385ca..c765baa 100644
--- a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionsResource.java
+++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDefinitionsResource.java
@@ -25,8 +25,8 @@ import static javax.ws.rs.core.Response.serverError;
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.HashSet;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -34,12 +34,14 @@ import javax.inject.Singleton;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import javax.xml.bind.annotation.XmlAccessType;
@@ -48,6 +50,7 @@ import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementWrapper;
 
 import org.apache.knox.gateway.service.definition.ServiceDefinitionPair;
+import org.apache.knox.gateway.service.definition.ServiceDefinitionPairComparator;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.registry.ServiceDefinitionRegistry;
@@ -69,68 +72,71 @@ public class ServiceDefinitionsResource {
   @GET
   @Produces({ APPLICATION_JSON, APPLICATION_XML })
   @Path("servicedefinitions")
-  public ServiceDefinitionsWrapper getServiceDefinitions() {
-    return getServiceDefinitions((Predicate<? super ServiceDefinitionPair>) null);
+  public ServiceDefinitionsWrapper getServiceDefinitions(@QueryParam("serviceOnly") @DefaultValue("false") boolean serviceOnly) {
+    return getServiceDefinitions((Predicate<? super ServiceDefinitionPair>) null, serviceOnly);
   }
 
   @GET
   @Produces({ APPLICATION_JSON, APPLICATION_XML })
   @Path("servicedefinitions/{name}")
-  public ServiceDefinitionsWrapper getServiceDefinition(@PathParam("name") String name) {
-    return getServiceDefinitions(serviceDefinitionPair -> serviceDefinitionPair.getService().getName().equalsIgnoreCase(name));
+  public ServiceDefinitionsWrapper getServiceDefinition(@PathParam("name") String name, @QueryParam("serviceOnly") @DefaultValue("false") boolean serviceOnly) {
+    return getServiceDefinitions(serviceDefinitionPair -> serviceDefinitionPair.getService().getName().equalsIgnoreCase(name), serviceOnly);
   }
 
   @GET
   @Produces({ APPLICATION_JSON, APPLICATION_XML })
   @Path("servicedefinitions/{name}/{role}")
-  public ServiceDefinitionsWrapper getServiceDefinition(@PathParam("name") String name, @PathParam("role") String role) {
+  public ServiceDefinitionsWrapper getServiceDefinition(@PathParam("name") String name, @PathParam("role") String role,
+      @QueryParam("serviceOnly") @DefaultValue("false") boolean serviceOnly) {
     return getServiceDefinitions(
-        serviceDefinitionPair -> serviceDefinitionPair.getService().getName().equalsIgnoreCase(name) && serviceDefinitionPair.getService().getRole().equalsIgnoreCase(role));
+        serviceDefinitionPair -> serviceDefinitionPair.getService().getName().equalsIgnoreCase(name) && serviceDefinitionPair.getService().getRole().equalsIgnoreCase(role),
+        serviceOnly);
   }
 
   @GET
   @Produces({ APPLICATION_JSON, APPLICATION_XML })
   @Path("servicedefinitions/{name}/{role}/{version}")
-  public ServiceDefinitionsWrapper getServiceDefinition(@PathParam("name") String name, @PathParam("role") String role, @PathParam("version") String version) {
+  public ServiceDefinitionsWrapper getServiceDefinition(@PathParam("name") String name, @PathParam("role") String role, @PathParam("version") String version,
+      @QueryParam("serviceOnly") @DefaultValue("false") boolean serviceOnly) {
     return getServiceDefinitions(serviceDefinitionPair -> serviceDefinitionPair.getService().getName().equalsIgnoreCase(name)
-        && serviceDefinitionPair.getService().getRole().equalsIgnoreCase(role) && serviceDefinitionPair.getService().getVersion().equalsIgnoreCase(version));
+        && serviceDefinitionPair.getService().getRole().equalsIgnoreCase(role) && serviceDefinitionPair.getService().getVersion().equalsIgnoreCase(version), serviceOnly);
   }
 
   @POST
   @Consumes({ APPLICATION_XML })
-  @Produces({ APPLICATION_JSON })
+  @Produces({ APPLICATION_JSON, APPLICATION_XML })
   @Path("servicedefinitions")
   public Response saveServiceDefinition(ServiceDefinitionPair serviceDefinition) {
     try {
       getServiceDefinitionRegistry().saveServiceDefinition(serviceDefinition);
       return created(toUri(serviceDefinition)).build();
     } catch (URISyntaxException | ServiceDefinitionRegistryException e) {
-      return serverError().entity("{ \"" +  ERROR_CODE_CREATION + "\": \"" + e.getMessage() + "\" }").build();
+      return serverError().entity("{ \"" + ERROR_CODE_CREATION + "\": \"" + e.getMessage() + "\" }").build();
     }
   }
 
   @PUT
   @Consumes({ APPLICATION_XML })
-  @Produces({ APPLICATION_JSON })
+  @Produces({ APPLICATION_JSON, APPLICATION_XML })
   @Path("servicedefinitions")
   public Response saveOrUpdateServiceDefinition(ServiceDefinitionPair serviceDefinition) {
     try {
       getServiceDefinitionRegistry().saveOrUpdateServiceDefinition(serviceDefinition);
       return created(toUri(serviceDefinition)).build();
     } catch (URISyntaxException | ServiceDefinitionRegistryException e) {
-      return serverError().entity("{ \"" +  ERROR_CODE_CREATION_OR_UPDATE + "\": \"" + e.getMessage() + "\" }").build();
+      return serverError().entity("{ \"" + ERROR_CODE_CREATION_OR_UPDATE + "\": \"" + e.getMessage() + "\" }").build();
     }
   }
 
   @DELETE
-  @Produces({ APPLICATION_JSON })
+  @Produces({ APPLICATION_JSON, APPLICATION_XML })
   @Path("servicedefinitions/{name}/{role}/{version}")
   public Response deleteServiceDefinition(@PathParam("name") String name, @PathParam("role") String role, @PathParam("version") String version) {
     try {
       getServiceDefinitionRegistry().deleteServiceDefinition(name, role, version);
       return ok().location(toUri(name, role, version)).build();
     } catch (URISyntaxException | ServiceDefinitionRegistryException e) {
-      return serverError().entity("{ \"" +  ERROR_CODE_DELETION + "\": \"" + e.getMessage() + "\" }").build();
+      return serverError().entity("{ \"" + ERROR_CODE_DELETION + "\": \"" + e.getMessage() + "\" }").build();
     }
   }
 
@@ -142,13 +148,24 @@ public class ServiceDefinitionsResource {
     return new URI("api/v1/servicedefinitions/" + name + "/" + role + "/" + version);
   }
 
-  private ServiceDefinitionsWrapper getServiceDefinitions(Predicate<? super ServiceDefinitionPair> predicate) {
+  private ServiceDefinitionsWrapper getServiceDefinitions(Predicate<? super ServiceDefinitionPair> predicate, boolean serviceOnly) {
+    final Set<ServiceDefinitionPair> serviceDefinitions = getServiceDefinitions(predicate);
+
     final ServiceDefinitionsWrapper serviceDefinitionsWrapper = new ServiceDefinitionsWrapper();
-    final Set<ServiceDefinitionPair> serviceDefinitions = getServiceDefinitionRegistry().getServiceDefinitions();
-    serviceDefinitionsWrapper.setServiceDefinitions(predicate == null ? serviceDefinitions : serviceDefinitions.stream().filter(predicate).collect(Collectors.toSet()));
+    if (serviceOnly) {
+      serviceDefinitions.stream()
+      .forEach(serviceDefinition -> serviceDefinitionsWrapper.getServiceDefinitions().add(new ServiceDefinitionPair(serviceDefinition.getService(), null)));
+    } else {
+      serviceDefinitionsWrapper.setServiceDefinitions(serviceDefinitions);
+    }
     return serviceDefinitionsWrapper;
   }
 
+  private Set<ServiceDefinitionPair> getServiceDefinitions(Predicate<? super ServiceDefinitionPair> predicate) {
+    final Set<ServiceDefinitionPair> serviceDefinitions = getServiceDefinitionRegistry().getServiceDefinitions();
+    return predicate == null ? serviceDefinitions : serviceDefinitions.stream().filter(predicate).collect(Collectors.toSet());
+  }
+
   private ServiceDefinitionRegistry getServiceDefinitionRegistry() {
     if (serviceDefinitionRegistry == null) {
       final GatewayServices gatewayServices = (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
@@ -157,12 +174,12 @@ public class ServiceDefinitionsResource {
     return serviceDefinitionRegistry;
   }
 
-  @XmlAccessorType(XmlAccessType.FIELD)
+  @XmlAccessorType(XmlAccessType.NONE)
   public static class ServiceDefinitionsWrapper {
 
-    @XmlElement(name="serviceDefinition")
+    @XmlElement(name = "serviceDefinition")
     @XmlElementWrapper(name = "serviceDefinitions")
-    private Set<ServiceDefinitionPair> serviceDefinitions = new HashSet<>();
+    private Set<ServiceDefinitionPair> serviceDefinitions = new TreeSet<>(new ServiceDefinitionPairComparator());
 
     public Set<ServiceDefinitionPair> getServiceDefinitions() {
       return serviceDefinitions;
diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java
index 4251c48..2199da4 100644
--- a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java
+++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/TopologiesResource.java
@@ -113,19 +113,23 @@ public class TopologiesResource {
                 (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
       for (org.apache.knox.gateway.topology.Topology t : ts.getTopologies()) {
         if (t.getName().equals(id)) {
+          // we need to convert first so that the original topology does not get
+          // overwritten in TopologyService (i.e. URI does not change from 'file://...' to
+          // 'https://...'
+          final Topology convertedTopology = BeanConverter.getTopology(t);
           try {
-            t.setUri(new URI( buildURI(t, config, request) ));
+            convertedTopology.setUri(new URI( buildURI(t, config, request) ));
           } catch (URISyntaxException se) {
-            t.setUri(null);
+            convertedTopology.setUri(null);
           }
 
           // For any read-only override topology, mark it as generated to discourage modification.
-          List<String> ambariManagedTopos = config.getReadOnlyOverrideTopologyNames();
-          if (ambariManagedTopos.contains(t.getName())) {
-            t.setGenerated(true);
+          final List<String> ambariManagedTopos = config.getReadOnlyOverrideTopologyNames();
+          if (ambariManagedTopos.contains(convertedTopology.getName())) {
+            convertedTopology.setGenerated(true);
           }
 
-          return BeanConverter.getTopology(t);
+          return convertedTopology;
         }
       }
     }


Mime
View raw message