brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From s...@apache.org
Subject [3/6] brooklyn-server git commit: TypeCoercions: support more generic coercers
Date Tue, 23 May 2017 14:23:41 GMT
TypeCoercions: support more generic coercers


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/b945fdc0
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/b945fdc0
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/b945fdc0

Branch: refs/heads/master
Commit: b945fdc08c41d1c3c944e9a41663d52902426baa
Parents: 377454e
Author: Aled Sage <aled.sage@gmail.com>
Authored: Mon May 22 14:09:54 2017 +0100
Committer: Aled Sage <aled.sage@gmail.com>
Committed: Tue May 23 14:45:33 2017 +0100

----------------------------------------------------------------------
 .../brooklyn/util/core/flags/TypeCoercions.java |  15 ++-
 .../coerce/CommonAdaptorTryCoercions.java       | 110 +++++++++++++++++++
 .../coerce/CommonAdaptorTypeCoercions.java      |   6 +-
 .../util/javalang/coerce/TryCoercer.java        |  45 ++++++++
 .../javalang/coerce/TypeCoercerExtensible.java  |  76 ++++---------
 .../coerce/TypeCoercerExtensibleTest.java       |  82 ++++++++++++++
 6 files changed, 269 insertions(+), 65 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java
index 9b39a56..9bc25e2 100644
--- a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java
+++ b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java
@@ -18,9 +18,6 @@
  */
 package org.apache.brooklyn.util.core.flags;
 
-import groovy.lang.Closure;
-import groovy.time.TimeDuration;
-
 import java.lang.reflect.Constructor;
 import java.util.Map;
 
@@ -40,19 +37,25 @@ import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.javalang.Boxing;
 import org.apache.brooklyn.util.javalang.JavaClassNames;
 import org.apache.brooklyn.util.javalang.Reflections;
+import org.apache.brooklyn.util.javalang.coerce.CommonAdaptorTryCoercions;
 import org.apache.brooklyn.util.javalang.coerce.CommonAdaptorTypeCoercions;
 import org.apache.brooklyn.util.javalang.coerce.EnumTypeCoercions;
 import org.apache.brooklyn.util.javalang.coerce.PrimitiveStringTypeCoercions;
+import org.apache.brooklyn.util.javalang.coerce.TryCoercer;
 import org.apache.brooklyn.util.javalang.coerce.TypeCoercer;
 import org.apache.brooklyn.util.javalang.coerce.TypeCoercerExtensible;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.annotations.Beta;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.reflect.TypeToken;
 
+import groovy.lang.Closure;
+import groovy.time.TimeDuration;
+
 /** Static class providing a shared {@link TypeCoercer} for all of Brooklyn */
 public class TypeCoercions {
 
@@ -68,6 +71,7 @@ public class TypeCoercions {
     
     public static void initStandardAdapters() {
         new BrooklynCommonAdaptorTypeCoercions(coercer).registerAllAdapters();
+        new CommonAdaptorTryCoercions(coercer).registerAllAdapters();
         registerDeprecatedBrooklynAdapters();
         registerBrooklynAdapters();
         registerGroovyAdapters();
@@ -82,6 +86,11 @@ public class TypeCoercions {
         return coercer.registerAdapter(sourceType, targetType, fn);
     }
     
+    @Beta
+    public static void registerAdapter(TryCoercer fn) {
+        coercer.registerAdapter(fn);
+    }
+    
     public static <T> Function<Object, T> function(final Class<T> type)
{
         return coercer.function(type);
     }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java
b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java
new file mode 100644
index 0000000..b0a494a
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTryCoercions.java
@@ -0,0 +1,110 @@
+/*
+ * 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.brooklyn.util.javalang.coerce;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+
+import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.text.Strings;
+
+import com.google.common.collect.Lists;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * Defines and registers common generic coercers (e.g. to call a "fromXyz" method, to 
+ * convert from one type to another).
+ */
+public class CommonAdaptorTryCoercions {
+
+    private final TypeCoercerExtensible coercer;
+
+    public CommonAdaptorTryCoercions(TypeCoercerExtensible coercer) {
+        this.coercer = coercer;
+    }
+
+    public CommonAdaptorTryCoercions registerAllAdapters() {
+        registerAdapter(new TryCoercerWithFromMethod());
+        registerAdapter(new TryCoercerToEnum());
+        registerAdapter(new TryCoercerForPrimitivesAndStrings());
+        return this;
+    }
+    
+    /** Registers an adapter for use with type coercion. */
+    public synchronized void registerAdapter(TryCoercer fn) {
+        coercer.registerAdapter(fn);
+    }
+    
+    protected static class TryCoercerWithFromMethod implements TryCoercer {
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> targetType)
{
+            Class<? super T> rawTargetType = targetType.getRawType();
+            
+            List<ClassCoercionException> exceptions = Lists.newArrayList();
+            //now look for static TargetType.fromType(Type t) where value instanceof Type
 
+            for (Method m: rawTargetType.getMethods()) {
+                if (((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC) &&

+                        m.getName().startsWith("from") && m.getParameterTypes().length==1
&&
+                        m.getParameterTypes()[0].isInstance(input)) {
+                    if (m.getName().equals("from"+JavaClassNames.verySimpleClassName(m.getParameterTypes()[0])))
{
+                        try {
+                            return Maybe.of((T) m.invoke(null, input));
+                        } catch (Exception e) {
+                            exceptions.add(new ClassCoercionException("Cannot coerce type
"+input.getClass()+" to "+rawTargetType.getCanonicalName()+" ("+input+"): "+m.getName()+"
adapting failed", e));
+                        }
+                    }
+                }
+            }
+            if (exceptions.isEmpty()) {
+                return null;
+            } else if (exceptions.size() == 1) {
+                return Maybe.absent(exceptions.get(0));
+            } else {
+                String errMsg = "Failed coercing type "+input.getClass()+" to "+rawTargetType.getCanonicalName();
+                return Maybe.absent(new CompoundRuntimeException(errMsg, exceptions));
+            }
+        }
+    }
+    
+    protected static class TryCoercerToEnum implements TryCoercer {
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> targetType)
{
+            Class<? super T> rawTargetType = targetType.getRawType();
+            
+            //for enums call valueOf with the string representation of the value
+            if (rawTargetType.isEnum()) {
+                return EnumTypeCoercions.tryCoerceUntyped(Strings.toString(input), (Class<T>)rawTargetType);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    protected static class TryCoercerForPrimitivesAndStrings implements TryCoercer {
+        @Override
+        public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> targetType)
{
+            return PrimitiveStringTypeCoercions.tryCoerce(input, targetType.getRawType());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java
b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java
index 03745e6..b74548e 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/CommonAdaptorTypeCoercions.java
@@ -41,8 +41,8 @@ import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.net.Cidr;
 import org.apache.brooklyn.util.net.Networking;
 import org.apache.brooklyn.util.net.UserAndHostAndPort;
-import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
+import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 import org.apache.brooklyn.util.time.Time;
 import org.apache.brooklyn.util.yaml.Yamls;
@@ -61,10 +61,6 @@ public class CommonAdaptorTypeCoercions {
         this.coercer = coercer;
     }
 
-    public TypeCoercerExtensible getCoercer() {
-        return coercer;
-    }
-    
     public CommonAdaptorTypeCoercions registerAllAdapters() {
         registerStandardAdapters();
         registerRecursiveIterableAdapters();

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java
b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java
new file mode 100644
index 0000000..f410fbc
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TryCoercer.java
@@ -0,0 +1,45 @@
+/*
+ * 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.brooklyn.util.javalang.coerce;
+
+import org.apache.brooklyn.util.guava.Maybe;
+
+import com.google.common.annotations.Beta;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * A coercer that can be registered, which will try to coerce the given input to the given
type.
+ * 
+ * This can be used for "generic" coercers, such as those that look for a {@code fromValue()}

+ * method on the target type.
+ */
+@Beta
+public interface TryCoercer {
+
+    /**
+     * The meaning of the return value is:
+     * <ul>
+     *   <li>null - no errors, recommend continue with fallbacks (i.e. not found).
+     *   <li>absent - had some kind of exception, recommend continue with fallbacks
(but can report this error if
+     *       other fallbacks fail).
+     *   <li>present - coercion successful.
+     * </ul>
+     */
+    <T> Maybe<T> tryCoerce(Object input, TypeToken<T> type);
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
index 366ae26..a673131 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
@@ -18,8 +18,6 @@
  */
 package org.apache.brooklyn.util.javalang.coerce;
 
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.Collection;
@@ -28,17 +26,15 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.javalang.Boxing;
-import org.apache.brooklyn.util.javalang.JavaClassNames;
-import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 import org.apache.brooklyn.util.time.Time;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.annotations.Beta;
 import com.google.common.base.Function;
 import com.google.common.base.Objects;
 import com.google.common.collect.HashBasedTable;
@@ -77,7 +73,10 @@ public class TypeCoercerExtensible implements TypeCoercer {
     /** has all the strategies (primitives, collections, etc) 
      * and all the adapters from {@link CommonAdaptorTypeCoercions} */
     public static TypeCoercerExtensible newDefault() {
-        return new CommonAdaptorTypeCoercions(newEmpty()).registerAllAdapters().getCoercer();
+        TypeCoercerExtensible result = newEmpty();
+        new CommonAdaptorTypeCoercions(result).registerAllAdapters();
+        new CommonAdaptorTryCoercions(result).registerAllAdapters();
+        return result;
     }
 
     /** has all the strategies (primitives, collections, etc) but no adapters, 
@@ -87,7 +86,10 @@ public class TypeCoercerExtensible implements TypeCoercer {
     }
 
     /** Store the coercion {@link Function functions} in a {@link Table table}. */
-    private Table<Class<?>, Class<?>, Function<?,?>> registry = HashBasedTable.create();
+    private final Table<Class<?>, Class<?>, Function<?,?>> registry
= HashBasedTable.create();
+
+    /** Store the generic coercers. */
+    private final List<TryCoercer> genericCoercers = Lists.newCopyOnWriteArrayList();
 
     @Override
     public <T> T coerce(Object value, Class<T> targetType) {
@@ -142,13 +144,11 @@ public class TypeCoercerExtensible implements TypeCoercer {
         
         if (targetType.isInstance(value)) return Maybe.of( (T) value );
 
-        result = PrimitiveStringTypeCoercions.tryCoerce(value, targetType);
-        if (result!=null && result.isPresent()) return result;
-        if (result!=null && firstError==null) firstError = result;
-        
-        result = tryCoerceWithFromMethod(value, targetType);
-        if (result!=null && result.isPresent()) return result;
-        if (result!=null && firstError==null) firstError = result;
+        for (TryCoercer coercer : genericCoercers) {
+            result = coercer.tryCoerce(value, targetTypeToken);
+            if (result!=null && result.isPresent()) return result;
+            if (result!=null && firstError==null) firstError = result;
+        }
         
         //ENHANCEMENT could look in type hierarchy of both types for a conversion method...
         
@@ -165,13 +165,6 @@ public class TypeCoercerExtensible implements TypeCoercer {
             }
         }
         
-        //for enums call valueOf with the string representation of the value
-        if (targetType.isEnum()) {
-            result = EnumTypeCoercions.tryCoerceUntyped(Strings.toString(value), (Class<T>)targetType);
-            if (result!=null && result.isPresent()) return result;
-            if (result!=null && firstError==null) firstError = result;
-        }
-
         //now look in registry
         synchronized (registry) {
             Map<Class<?>, Function<?,?>> adapters = registry.row(targetType);
@@ -212,42 +205,6 @@ public class TypeCoercerExtensible implements TypeCoercer {
         return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+"
to "+targetType.getCanonicalName()+" ("+value+"): no adapter known"));
     }
 
-    /**
-     * The meaning of the return value is:
-     * <ul>
-     *   <li>null - no errors, continue with fallbacks (i.e. not found).
-     *   <li>absent - had some kind of exception, continue with fallbacks (but can
report this error if
-     *       other fallbacks fail).
-     *   <li>present - coercion successful, return value.
-     * </ul>
-     */
-    @SuppressWarnings("unchecked")
-    protected <T> Maybe<T> tryCoerceWithFromMethod(Object value, Class<? super
T> targetType) {
-        List<ClassCoercionException> exceptions = Lists.newArrayList();
-        //now look for static TargetType.fromType(Type t) where value instanceof Type  
-        for (Method m: targetType.getMethods()) {
-            if (((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC) && 
-                    m.getName().startsWith("from") && m.getParameterTypes().length==1
&&
-                    m.getParameterTypes()[0].isInstance(value)) {
-                if (m.getName().equals("from"+JavaClassNames.verySimpleClassName(m.getParameterTypes()[0])))
{
-                    try {
-                        return Maybe.of((T) m.invoke(null, value));
-                    } catch (Exception e) {
-                        exceptions.add(new ClassCoercionException("Cannot coerce type "+value.getClass()+"
to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed", e));
-                    }
-                }
-            }
-        }
-        if (exceptions.isEmpty()) {
-            return null;
-        } else if (exceptions.size() == 1) {
-            return Maybe.absent(exceptions.get(0));
-        } else {
-            String errMsg = "Failed coercing type "+value.getClass()+" to "+targetType.getCanonicalName();
-            return Maybe.absent(new CompoundRuntimeException(errMsg, exceptions));
-        }
-    }
-
     @SuppressWarnings("unchecked")
     protected <T> Maybe<T> tryCoerceMap(Object value, TypeToken<T> targetTypeToken)
{
         if (!(value instanceof Map) || !(Map.class.isAssignableFrom(targetTypeToken.getRawType())))
return null;
@@ -324,4 +281,9 @@ public class TypeCoercerExtensible implements TypeCoercer {
         return (Function<? super A,B>) registry.put(targetType, sourceType, fn);
     }
     
+    /** Registers a generic adapter for use with type coercion. */
+    @Beta
+    public synchronized void registerAdapter(TryCoercer fn) {
+        genericCoercers.add(fn);
+    }
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/b945fdc0/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.java
----------------------------------------------------------------------
diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.java
b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.java
new file mode 100644
index 0000000..afb10f9
--- /dev/null
+++ b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensibleTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.brooklyn.util.javalang.coerce;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Objects;
+
+import org.apache.brooklyn.util.guava.Maybe;
+import org.testng.annotations.Test;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.reflect.TypeToken;
+
+public class TypeCoercerExtensibleTest {
+
+    TypeCoercerExtensible coercer = TypeCoercerExtensible.newDefault();
+    
+    protected <T> T coerce(Object x, Class<T> type) {
+        return coercer.coerce(x, type);
+    }
+    protected <T> T coerce(Object x, TypeToken<T> type) {
+        return coercer.coerce(x, type);
+    }
+    
+    @Test
+    public void testRegisterNewGenericCoercer() {
+        coercer.registerAdapter(new TryCoercer() {
+            @Override
+            @SuppressWarnings("unchecked")
+            public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> type)
{
+                if (input instanceof String && type.getRawType() == MyClazz.class)
{
+                    return (Maybe<T>) Maybe.of(new MyClazz("myprefix"+input));
+                } else {
+                    return null;
+                }
+            }
+        });
+        
+        assertEquals(coerce("abc", MyClazz.class), new MyClazz("myprefixabc"));
+    }
+    
+    public static class MyClazz {
+        private final String val;
+
+        public MyClazz(String val) {
+            this.val = val;
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(val);
+        }
+        
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof MyClazz))  return false;
+            return Objects.equals(val, ((MyClazz)obj).val);
+        }
+        
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this).add("val", val).toString();
+        }
+    }
+}


Mime
View raw message