knox-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From kris...@apache.org
Subject [knox] branch master updated: KNOX-1914 - New admin API to be used by the UI to fetch available service discovery types (#147)
Date Mon, 23 Sep 2019 14:16:22 GMT
This is an automated email from the ASF dual-hosted git repository.

krisden 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 51b9ab7  KNOX-1914 - New admin API to be used by the UI to fetch available service
discovery types (#147)
51b9ab7 is described below

commit 51b9ab77960eeb1a65248ac2f602d7903765ac82
Author: Sandor Molnar <smolnar@apache.org>
AuthorDate: Mon Sep 23 16:16:17 2019 +0200

    KNOX-1914 - New admin API to be used by the UI to fetch available service discovery types
(#147)
---
 .../new-desc-wizard/new-desc-wizard.component.html |   2 +-
 .../resource-detail/resource-detail.component.html |   2 +-
 .../admin-ui/app/resource/resource.service.ts      |  27 +++++-
 .../discovery/ambari/AmbariServiceDiscovery.java   |   2 +-
 .../discovery/ServiceDiscoveryFactory.java         |  81 ----------------
 .../discovery/ServiceDiscoveryFactoryTest.java     |  12 +++
 .../ServiceDiscoveryCollectionMarshaller.java      |  70 ++++++++++++++
 .../service/admin/ServiceDiscoveryResource.java    | 102 +++++++++++++++++++++
 gateway-spi/pom.xml                                |   4 +
 .../discovery/ServiceDiscoveryFactory.java         |  81 ++++++++++++++++
 10 files changed, 295 insertions(+), 88 deletions(-)

diff --git a/gateway-admin-ui/admin-ui/app/new-desc-wizard/new-desc-wizard.component.html
b/gateway-admin-ui/admin-ui/app/new-desc-wizard/new-desc-wizard.component.html
index c709991..960f300 100644
--- a/gateway-admin-ui/admin-ui/app/new-desc-wizard/new-desc-wizard.component.html
+++ b/gateway-admin-ui/admin-ui/app/new-desc-wizard/new-desc-wizard.component.html
@@ -85,7 +85,7 @@
                               <span>
                                 <select id="select" autofocus required class="md-select
form-control"
                                         [(ngModel)]="descriptor.discoveryType" (change)="descriptor.setDirty()">
-                                  <option *ngFor="let typeOption of resourceService.getSupportedDiscoveryTypes()"
+                                  <option *ngFor="let typeOption of resourceService.discoveryTypes"
                                           class="md-option"
                                           [value]="typeOption">{{typeOption}}</option>
                                 </select>
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 c110af3..f6bb824 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
@@ -366,7 +366,7 @@
                             <span>
                               <select id="select" autofocus required class="md-select
form-control"
                                       [(ngModel)]="descriptor.discoveryType" (change)="descriptor.setDirty()">
-                                <option *ngFor="let typeOption of resourceService.getSupportedDiscoveryTypes()"
+                                <option *ngFor="let typeOption of resourceService.discoveryTypes"
                                         class="md-option"
                                         [value]="typeOption"
                                         [selected]="descriptor.discoveryType === typeOption">{{typeOption}}</option>
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 4e6b624..a8fcc28 100644
--- a/gateway-admin-ui/admin-ui/app/resource/resource.service.ts
+++ b/gateway-admin-ui/admin-ui/app/resource/resource.service.ts
@@ -24,13 +24,13 @@ import {Descriptor} from '../resource-detail/descriptor';
 
 @Injectable()
 export class ResourceService {
-    // TODO: PJZ: Get this list dynamically?
-    private static discoveryTypes: Array<string> = ['ClouderaManager', 'Ambari'];
+    discoveryTypes: Array<string>;
 
     apiUrl = window.location.pathname.replace(new RegExp('admin-ui/.*'), 'api/v1/');
     providersUrl = this.apiUrl + 'providerconfig';
     descriptorsUrl = this.apiUrl + 'descriptors';
     topologiesUrl = this.apiUrl + 'topologies';
+    serviceDiscoveriesUrl = this.apiUrl + 'servicediscoveries';
 
     selectedResourceTypeSource = new Subject<string>();
     selectedResourceType$ = this.selectedResourceTypeSource.asObservable();
@@ -44,11 +44,30 @@ export class ResourceService {
     changedProviderConfigurationSource = new Subject<Array<ProviderConfig>>();
     changedProviderConfiguration$ = this.changedProviderConfigurationSource.asObservable();
 
+
     constructor(private http: HttpClient) {
+        this.initSupportedDiscoveryTypes();
+    }
+
+    initSupportedDiscoveryTypes(): void {
+        if (this.discoveryTypes == null || this.discoveryTypes.length === 0) {
+            let headers = this.addJsonHeaders(new HttpHeaders());
+            this.getServiceDiscoveryResources()
+            .then(response => this.discoveryTypes = response.knoxServiceDiscoveries.knoxServiceDiscovery.map(sd
=> sd.type))
+            .catch((err: HttpErrorResponse) => {
+                console.debug('ResourceService --> getServiceDiscoveryResources() -->
error: HTTP ' + err.status + ' ' + err.message);
+                if (err.status === 401) {
+                    window.location.assign(document.location.pathname);
+                } else {
+                    return this.handleError(err);
+                }
+            });
+        }
     }
 
-    getSupportedDiscoveryTypes(): string[] {
-        return ResourceService.discoveryTypes;
+    getServiceDiscoveryResources(): Promise<any> {
+        let headers = this.addJsonHeaders(new HttpHeaders());
+        return this.http.get(this.serviceDiscoveriesUrl, {headers: headers}).toPromise();
     }
 
     getResources(resType: string): Promise<Resource[]> {
diff --git a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariServiceDiscovery.java
b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariServiceDiscovery.java
index 6e891c7..cef6e9c 100644
--- a/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariServiceDiscovery.java
+++ b/gateway-discovery-ambari/src/main/java/org/apache/knox/gateway/topology/discovery/ambari/AmbariServiceDiscovery.java
@@ -47,7 +47,7 @@ import java.util.Properties;
 
 class AmbariServiceDiscovery implements ServiceDiscovery {
 
-    static final String TYPE = "AMBARI";
+    static final String TYPE = "Ambari";
 
     static final String AMBARI_CLUSTERS_URI = AmbariClientCommon.AMBARI_CLUSTERS_URI;
 
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactory.java
b/gateway-server/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactory.java
deleted file mode 100644
index f055f70..0000000
--- a/gateway-server/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactory.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.knox.gateway.topology.discovery;
-
-import org.apache.knox.gateway.services.Service;
-
-import java.lang.reflect.Field;
-import java.util.ServiceLoader;
-
-/**
- * Creates instances of ServiceDiscovery implementations.
- *
- * This factory uses the ServiceLoader mechanism to load ServiceDiscovery implementations
as extensions.
- *
- */
-public abstract class ServiceDiscoveryFactory {
-
-    private static final Service[] NO_GATEWAY_SERVICS = new Service[]{};
-
-
-    public static ServiceDiscovery get(String type) {
-        return get(type, NO_GATEWAY_SERVICS);
-    }
-
-
-    public static ServiceDiscovery get(String type, Service...gatewayServices) {
-        ServiceDiscovery sd  = null;
-
-        // Look up the available ServiceDiscovery types
-        ServiceLoader<ServiceDiscoveryType> loader = ServiceLoader.load(ServiceDiscoveryType.class);
-        for (ServiceDiscoveryType sdt : loader) {
-            if (sdt.getType().equalsIgnoreCase(type)) {
-                try {
-                    ServiceDiscovery instance = sdt.newInstance();
-                    // Make sure the type reported by the instance matches the type declared
by the factory
-                    // (is this necessary?)
-                    if (instance.getType().equalsIgnoreCase(type)) {
-                        sd = instance;
-
-                        // Inject any gateway services that were specified, and which are
referenced in the impl
-                        if (gatewayServices != null && gatewayServices.length >
0) {
-                            for (Field field : sd.getClass().getDeclaredFields()) {
-                                if (field.getDeclaredAnnotation(GatewayService.class) !=
null) {
-                                    for (Service s : gatewayServices) {
-                                        if (s != null) {
-                                            if (field.getType().isAssignableFrom(s.getClass()))
{
-                                                field.setAccessible(true);
-                                                field.set(sd, s);
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                        break;
-                    }
-                } catch (Exception e) {
-                    e.printStackTrace();
-                }
-            }
-        }
-
-        return sd;
-    }
-
-
-}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactoryTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactoryTest.java
index dacc998..7aa712d 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactoryTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactoryTest.java
@@ -22,6 +22,7 @@ import org.junit.Test;
 
 import java.lang.reflect.Field;
 import java.util.Locale;
+import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -80,5 +81,16 @@ public class ServiceDiscoveryFactoryTest {
         assertTrue(AliasService.class.isAssignableFrom(fieldValue.getClass()));
     }
 
+    @Test
+    public void testGetAllServiceDiscoveries() {
+      final Set<ServiceDiscovery> serviceDiscoveries = ServiceDiscoveryFactory.getAllServiceDiscoveries();
+      assertEquals(3, serviceDiscoveries.size());
+      assertTrue(hasServiceDiscoveryWithType(serviceDiscoveries, "DUMMY"));
+      assertTrue(hasServiceDiscoveryWithType(serviceDiscoveries, "PROPERTIES_FILE"));
+      assertTrue(hasServiceDiscoveryWithType(serviceDiscoveries, "ActualType"));
+    }
 
+    private boolean hasServiceDiscoveryWithType(Set<ServiceDiscovery> serviceDiscoveries,
String type) {
+      return serviceDiscoveries.stream().anyMatch(serviceDiscovery -> serviceDiscovery.getType().equalsIgnoreCase(type));
+    }
 }
diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDiscoveryCollectionMarshaller.java
b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDiscoveryCollectionMarshaller.java
new file mode 100644
index 0000000..6f93cd4
--- /dev/null
+++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDiscoveryCollectionMarshaller.java
@@ -0,0 +1,70 @@
+/*
+ * 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.knox.gateway.service.admin;
+
+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;
+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.ServiceDiscoveryResource.ServiceDiscoveryWrapper;
+import org.eclipse.persistence.jaxb.JAXBContextProperties;
+
+@Provider
+@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
+public class ServiceDiscoveryCollectionMarshaller implements MessageBodyWriter<ServiceDiscoveryResource.ServiceDiscoveryWrapper>
{
+
+  @Override
+  public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
+    return ServiceDiscoveryResource.ServiceDiscoveryWrapper.class == type;
+  }
+
+  @Override
+  public long getSize(ServiceDiscoveryWrapper t, Class<?> type, Type genericType, Annotation[]
annotations, MediaType mediaType) {
+    return -1;
+  }
+
+  @Override
+  public void writeTo(ServiceDiscoveryResource.ServiceDiscoveryWrapper instance, Class<?>
type, Type genericType, Annotation[] annotations, MediaType mediaType,
+      MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws
IOException, WebApplicationException {
+    try {
+      final Map<String, Object> properties = new HashMap<>(1);
+      properties.put(JAXBContextProperties.MEDIA_TYPE, mediaType.toString());
+      final JAXBContext context = JAXBContext.newInstance(new Class[] { ServiceDiscoveryResource.ServiceDiscoveryWrapper.class
}, properties);
+      final Marshaller m = context.createMarshaller();
+      m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+      m.marshal(instance, entityStream);
+    } catch (JAXBException e) {
+      throw new IOException(e);
+    }
+  }
+
+}
diff --git a/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDiscoveryResource.java
b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDiscoveryResource.java
new file mode 100644
index 0000000..5cbea74
--- /dev/null
+++ b/gateway-service-admin/src/main/java/org/apache/knox/gateway/service/admin/ServiceDiscoveryResource.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.knox.gateway.service.admin;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+
+import org.apache.knox.gateway.topology.discovery.ServiceDiscovery;
+import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryFactory;
+
+@Path("/api/v1")
+public class ServiceDiscoveryResource {
+
+  @GET
+  @Produces({ APPLICATION_JSON, APPLICATION_XML })
+  @Path("servicediscoveries")
+  public ServiceDiscoveryWrapper getServiceDiscoveries() {
+    final ServiceDiscoveryWrapper serviceDiscoveryWrapper = new ServiceDiscoveryWrapper();
+    serviceDiscoveryWrapper.setKnoxServiceDiscoveries(ServiceDiscoveryFactory.getAllServiceDiscoveries());
+    return serviceDiscoveryWrapper;
+  }
+
+  @XmlAccessorType(XmlAccessType.NONE)
+  public static class KnoxServiceDiscovery {
+
+    @XmlElement
+    private final String type;
+
+    @XmlElement
+    private final String implementation;
+
+    // having a no-argument constructor is required by JAXB
+    public KnoxServiceDiscovery() {
+      this(null, null);
+    }
+
+    public KnoxServiceDiscovery(String type, String implementation) {
+      this.type = type;
+      this.implementation = implementation;
+    }
+
+    public String getType() {
+      return type;
+    }
+
+    public String getImplementation() {
+      return implementation;
+    }
+
+  }
+
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class ServiceDiscoveryWrapper {
+
+    @XmlElement(name = "knoxServiceDiscovery")
+    @XmlElementWrapper(name = "knoxServiceDiscoveries")
+    private Set<KnoxServiceDiscovery> knoxServiceDiscoveries = new HashSet<>();
+
+    public Set<KnoxServiceDiscovery> getKnoxServiceDiscoveries() {
+      return knoxServiceDiscoveries;
+    }
+
+    public void setKnoxServiceDiscoveries(Set<KnoxServiceDiscovery> knoxServiceDiscoveries)
{
+      this.knoxServiceDiscoveries = knoxServiceDiscoveries;
+    }
+
+    void setKnoxServiceDiscoveries(Collection<ServiceDiscovery> serviceDiscoveries)
{
+      serviceDiscoveries.forEach(serviceDiscovery -> {
+        this.knoxServiceDiscoveries.add(new KnoxServiceDiscovery(serviceDiscovery.getType(),
serviceDiscovery.getClass().getCanonicalName()));
+
+      });
+    }
+  }
+}
diff --git a/gateway-spi/pom.xml b/gateway-spi/pom.xml
index c6dffab..3ddc691 100644
--- a/gateway-spi/pom.xml
+++ b/gateway-spi/pom.xml
@@ -118,6 +118,10 @@
             <groupId>joda-time</groupId>
             <artifactId>joda-time</artifactId>
         </dependency>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-api</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>io.dropwizard.metrics</groupId>
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactory.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactory.java
new file mode 100644
index 0000000..05b3398
--- /dev/null
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/topology/discovery/ServiceDiscoveryFactory.java
@@ -0,0 +1,81 @@
+/*
+ * 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.knox.gateway.topology.discovery;
+
+import java.lang.reflect.Field;
+import java.util.HashSet;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.knox.gateway.services.Service;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Creates instances of ServiceDiscovery implementations.
+ *
+ * This factory uses the ServiceLoader mechanism to load ServiceDiscovery
+ * implementations as extensions.
+ *
+ */
+public abstract class ServiceDiscoveryFactory {
+
+  private static final Logger LOG = LoggerFactory.getLogger(ServiceDiscoveryFactory.class);
+
+  private static final Service[] NO_GATEWAY_SERVICS = new Service[] {};
+
+  public static ServiceDiscovery get(String type) {
+    return get(type, NO_GATEWAY_SERVICS);
+  }
+
+  public static Set<ServiceDiscovery> getAllServiceDiscoveries() {
+    final Set<ServiceDiscovery> serviceDiscoveries = new HashSet<>();
+    ServiceLoader.load(ServiceDiscoveryType.class).forEach((serviceDiscoveryType) -> {
+      serviceDiscoveries.add(serviceDiscoveryType.newInstance());
+    });
+    return serviceDiscoveries;
+  }
+
+  public static ServiceDiscovery get(String type, Service... gatewayServices) {
+    final ServiceDiscovery sd = getAllServiceDiscoveries().stream().filter(serviceDiscovery
-> serviceDiscovery.getType().equalsIgnoreCase(type)).findFirst().orElse(null);
+    // Inject any gateway services that were specified, and which are referenced in the impl
+    injectGatewayServices(sd, gatewayServices);
+    return sd;
+  }
+
+  private static void injectGatewayServices(final ServiceDiscovery serviceDiscovery, Service...
gatewayServices) {
+    if (ArrayUtils.isNotEmpty(gatewayServices)) {
+      try {
+        for (Field field : serviceDiscovery.getClass().getDeclaredFields()) {
+          if (field.getDeclaredAnnotation(GatewayService.class) != null) {
+            for (Service gatewayService : gatewayServices) {
+              if (gatewayService != null) {
+                if (field.getType().isAssignableFrom(gatewayService.getClass())) {
+                  field.setAccessible(true);
+                  field.set(serviceDiscovery, gatewayService);
+                }
+              }
+            }
+          }
+        }
+      } catch (Exception e) {
+        LOG.error("Error while injecting Gateway Services in service discoveries", e);
+      }
+    }
+  }
+}


Mime
View raw message