cassandra-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From sn...@apache.org
Subject [1/3] cassandra git commit: Add tinyint, smallint, time, date support for UDFs
Date Thu, 04 Jun 2015 15:08:32 GMT
Repository: cassandra
Updated Branches:
  refs/heads/cassandra-2.2 0022e153f -> 30be921d4
  refs/heads/trunk f48c48307 -> 94ff7a5eb


Add tinyint,smallint,time,date support for UDFs

patch by Robert Stupp, reviewed by Sam Tunnicliffe CASSANDRA-9400


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/30be921d
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/30be921d
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/30be921d

Branch: refs/heads/cassandra-2.2
Commit: 30be921d44ed62f0c29a40dc841190519f84cffc
Parents: 0022e15
Author: Robert Stupp <snazy@snazy.de>
Authored: Thu Jun 4 17:06:02 2015 +0200
Committer: Robert Stupp <snazy@snazy.de>
Committed: Thu Jun 4 17:07:47 2015 +0200

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 .../cql3/functions/ScriptBasedUDF.java          |   4 +
 .../cassandra/cql3/functions/UDFunction.java    |  14 +
 test/unit/org/apache/cassandra/cql3/UFTest.java | 317 ++++++++++---------
 4 files changed, 188 insertions(+), 148 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/30be921d/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index 47ed221..43a6cc5 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 2.2
+ * Add tinyint,smallint,time,date support for UDFs (CASSANDRA-9400)
  * Deprecates SSTableSimpleWriter and SSTableSimpleUnsortedWriter (CASSANDRA-9546)
  * Empty INITCOND treated as null in aggregate (CASSANDRA-9457)
  * Remove use of Cell in Thrift MapReduce classes (CASSANDRA-8609)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/30be921d/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java b/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java
index 319c948..4d9a79f 100644
--- a/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java
+++ b/src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDF.java
@@ -114,6 +114,10 @@ public class ScriptBasedUDF extends UDFunction
                     Number rNumber = (Number) result;
                     if (javaReturnType == Integer.class)
                         result = rNumber.intValue();
+                    else if (javaReturnType == Short.class)
+                        result = rNumber.shortValue();
+                    else if (javaReturnType == Byte.class)
+                        result = rNumber.byteValue();
                     else if (javaReturnType == Long.class)
                         result = rNumber.longValue();
                     else if (javaReturnType == Float.class)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/30be921d/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
index 0bf6078..aa6d555 100644
--- a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
@@ -212,6 +212,20 @@ public abstract class UDFunction extends AbstractFunction implements
ScalarFunct
     }
 
     // do not remove - used by generated Java UDFs
+    protected byte compose_byte(int protocolVersion, int argIndex, ByteBuffer value)
+    {
+        assert value != null && value.remaining() > 0;
+        return (byte)DataType.tinyint().deserialize(value, ProtocolVersion.fromInt(protocolVersion));
+    }
+
+    // do not remove - used by generated Java UDFs
+    protected short compose_short(int protocolVersion, int argIndex, ByteBuffer value)
+    {
+        assert value != null && value.remaining() > 0;
+        return (short)DataType.smallint().deserialize(value, ProtocolVersion.fromInt(protocolVersion));
+    }
+
+    // do not remove - used by generated Java UDFs
     protected int compose_int(int protocolVersion, int argIndex, ByteBuffer value)
     {
         assert value != null && value.remaining() > 0;

http://git-wip-us.apache.org/repos/asf/cassandra/blob/30be921d/test/unit/org/apache/cassandra/cql3/UFTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/cql3/UFTest.java b/test/unit/org/apache/cassandra/cql3/UFTest.java
index f041b3a..db94a4c 100644
--- a/test/unit/org/apache/cassandra/cql3/UFTest.java
+++ b/test/unit/org/apache/cassandra/cql3/UFTest.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.UUID;
 
 import org.junit.Assert;
 import org.junit.Test;
@@ -44,6 +45,7 @@ import org.apache.cassandra.transport.Event;
 import org.apache.cassandra.transport.Server;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.UUIDGen;
 
 public class UFTest extends CQLTester
 {
@@ -1872,6 +1874,10 @@ public class UFTest extends CQLTester
         Object[][] variations = {
                                 new Object[]    {   "true",     "boolean",  true    },
                                 new Object[]    {   "false",    "boolean",  false   },
+                                new Object[]    {   "100",      "tinyint",  (byte)100 },
+                                new Object[]    {   "100.",     "tinyint",  (byte)100 },
+                                new Object[]    {   "100",      "smallint", (short)100 },
+                                new Object[]    {   "100.",     "smallint", (short)100 },
                                 new Object[]    {   "100",      "int",      100     },
                                 new Object[]    {   "100.",     "int",      100     },
                                 new Object[]    {   "100",      "double",   100d    },
@@ -1904,17 +1910,26 @@ public class UFTest extends CQLTester
     @Test
     public void testScriptParamReturnTypes() throws Throwable
     {
-        createTable("CREATE TABLE %s (key int primary key, ival int, lval bigint, fval float,
dval double, vval varint, ddval decimal)");
-        execute("INSERT INTO %s (key, ival, lval, fval, dval, vval, ddval) VALUES (?, ?,
?, ?, ?, ?, ?)", 1,
-                1, 1L, 1f, 1d, BigInteger.valueOf(1L), BigDecimal.valueOf(1d));
+        UUID ruuid = UUID.randomUUID();
+        UUID tuuid = UUIDGen.getTimeUUID();
+
+        createTable("CREATE TABLE %s (key int primary key, " +
+                    "tival tinyint, sival smallint, ival int, lval bigint, fval float, dval
double, vval varint, ddval decimal, " +
+                    "timval time, dtval date, tsval timestamp, uval uuid, tuval timeuuid)");
+        execute("INSERT INTO %s (key, tival, sival, ival, lval, fval, dval, vval, ddval,
timval, dtval, tsval, uval, tuval) VALUES " +
+                "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 1,
+                (byte)1, (short)1, 1, 1L, 1f, 1d, BigInteger.valueOf(1L), BigDecimal.valueOf(1d),
1L, Integer.MAX_VALUE, new Date(1), ruuid, tuuid);
 
         Object[][] variations = {
+                                new Object[] {  "tinyint",  "tival",    (byte)1,        
       (byte)2  },
+                                new Object[] {  "smallint", "sival",    (short)1,       
       (short)2  },
                                 new Object[] {  "int",      "ival",     1,              
       2  },
                                 new Object[] {  "bigint",   "lval",     1L,             
       2L  },
                                 new Object[] {  "float",    "fval",     1f,             
       2f  },
                                 new Object[] {  "double",   "dval",     1d,             
       2d  },
                                 new Object[] {  "varint",   "vval",     BigInteger.valueOf(1L),
BigInteger.valueOf(2L)  },
                                 new Object[] {  "decimal",  "ddval",    BigDecimal.valueOf(1d),
BigDecimal.valueOf(2d)  },
+                                new Object[] {  "time",     "timval",   1L,             
       2L  },
                                 };
 
         for (Object[] variation : variations)
@@ -1932,81 +1947,162 @@ public class UFTest extends CQLTester
             assertRows(execute("SELECT key, " + col + ", " + fName + '(' + col + ") FROM
%s"),
                        row(1, expected1, expected2));
         }
+
+        variations = new Object[][] {
+                     new Object[] {  "timestamp","tsval",    new Date(1),            new
Date(1)  },
+                     new Object[] {  "uuid",     "uval",     ruuid,                  ruuid
 },
+                     new Object[] {  "timeuuid", "tuval",    tuuid,                  tuuid
 },
+                     new Object[] {  "date",     "dtval",    Integer.MAX_VALUE,      Integer.MAX_VALUE
},
+        };
+
+        for (Object[] variation : variations)
+        {
+            Object type = variation[0];
+            Object col = variation[1];
+            Object expected1 = variation[2];
+            Object expected2 = variation[3];
+            String fName = createFunction(KEYSPACE, type.toString(),
+                                          "CREATE OR REPLACE FUNCTION %s(val " + type + ")
" +
+                                          "RETURNS NULL ON NULL INPUT " +
+                                          "RETURNS " + type + ' ' +
+                                          "LANGUAGE javascript " +
+                                          "AS 'val;';");
+            assertRows(execute("SELECT key, " + col + ", " + fName + '(' + col + ") FROM
%s"),
+                       row(1, expected1, expected2));
+        }
+    }
+
+    static class TypesTestDef
+    {
+        final String udfType;
+        final String tableType;
+        final String columnName;
+        final Object referenceValue;
+
+        String fCheckArgAndReturn;
+
+        String fCalledOnNull;
+        String fReturnsNullOnNull;
+
+        TypesTestDef(String udfType, String tableType, String columnName, Object referenceValue)
+        {
+            this.udfType = udfType;
+            this.tableType = tableType;
+            this.columnName = columnName;
+            this.referenceValue = referenceValue;
+        }
     }
 
     @Test
-    public void testNullOnReturnsNullOnNullInput() throws Throwable
+    public void testTypesWithAndWithoutNulls() throws Throwable
     {
+        // test various combinations of types against UDFs with CALLED ON NULL or RETURNS
NULL ON NULL
+
         String type = createType("CREATE TYPE %s (txt text, i int)");
-        createTable("CREATE TABLE %s (key int PRIMARY KEY, i int, b bigint, f float, d double,
x boolean, t text, u frozen<"+type+">, tup frozen<tuple<int, text>>)");
-
-        execute("INSERT INTO %s (key, i, b, f, d, x, t, u, tup) VALUES (1, null, null, null,
null, null, null, null, null)");
-
-        String fI = createFunction(KEYSPACE,
-                                   "int",
-                                   "CREATE OR REPLACE FUNCTION %s(val int) " +
-                                   "RETURNS NULL ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fB = createFunction(KEYSPACE,
-                                   "bigint",
-                                   "CREATE OR REPLACE FUNCTION %s(val bigint) " +
-                                   "RETURNS NULL ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fF = createFunction(KEYSPACE,
-                                   "float",
-                                   "CREATE OR REPLACE FUNCTION %s(val float) " +
-                                   "RETURNS NULL ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fD = createFunction(KEYSPACE,
-                                   "double",
-                                   "CREATE OR REPLACE FUNCTION %s(val double) " +
-                                   "RETURNS NULL ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fX = createFunction(KEYSPACE,
-                                   "boolean",
-                                   "CREATE OR REPLACE FUNCTION %s(val boolean) " +
-                                   "RETURNS NULL ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fT = createFunction(KEYSPACE,
-                                   "text",
-                                   "CREATE OR REPLACE FUNCTION %s(val text) " +
-                                   "RETURNS NULL ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fU = createFunction(KEYSPACE,
-                                   type,
-                                   "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
-                                   "RETURNS NULL ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fTup = createFunction(KEYSPACE,
-                                     "tuple<int, text>",
-                                     "CREATE OR REPLACE FUNCTION %s(val tuple<int, text>)
" +
-                                     "RETURNS NULL ON NULL INPUT " +
-                                     "RETURNS text " +
-                                     "LANGUAGE java\n" +
-                                     "AS 'return \"foo bar\";';");
-
-        assertRows(execute("SELECT " + fI + "(i) FROM %s WHERE key=1"), row(new Object[]{null}));
-        assertRows(execute("SELECT " + fB + "(b) FROM %s WHERE key=1"), row(new Object[]{null}));
-        assertRows(execute("SELECT " + fF + "(f) FROM %s WHERE key=1"), row(new Object[]{null}));
-        assertRows(execute("SELECT " + fD + "(d) FROM %s WHERE key=1"), row(new Object[]{null}));
-        assertRows(execute("SELECT " + fX + "(x) FROM %s WHERE key=1"), row(new Object[]{null}));
-        assertRows(execute("SELECT " + fT + "(t) FROM %s WHERE key=1"), row(new Object[]{null}));
-        assertRows(execute("SELECT " + fU + "(u) FROM %s WHERE key=1"), row(new Object[]{null}));
-        assertRows(execute("SELECT " + fTup + "(tup) FROM %s WHERE key=1"), row(new Object[]{null}));
+
+        TypesTestDef[] typeDefs =
+        {
+        //                udf type,            table type,                 column, reference
value
+        new TypesTestDef("timestamp", "timestamp", "ts", new Date()),
+        new TypesTestDef("date", "date", "dt", 12345),
+        new TypesTestDef("time", "time", "tim", 12345L),
+        new TypesTestDef("uuid", "uuid", "uu", UUID.randomUUID()),
+        new TypesTestDef("timeuuid", "timeuuid", "tu", UUIDGen.getTimeUUID()),
+        new TypesTestDef("tinyint", "tinyint", "ti", (byte) 42),
+        new TypesTestDef("smallint", "smallint", "si", (short) 43),
+        new TypesTestDef("int", "int", "i", 44),
+        new TypesTestDef("bigint", "bigint", "b", 45L),
+        new TypesTestDef("float", "float", "f", 46f),
+        new TypesTestDef("double", "double", "d", 47d),
+        new TypesTestDef("boolean", "boolean", "x", true),
+        new TypesTestDef("ascii", "ascii", "a", "tqbfjutld"),
+        new TypesTestDef("text", "text", "t", "k\u00f6lsche jung"),
+        //new TypesTestDef(type,                 "frozen<" + type + '>',     "u", 
  null),
+        new TypesTestDef("tuple<int, text>", "frozen<tuple<int, text>>",
"tup", tuple(1, "foo"))
+        };
+
+        String createTableDDL = "CREATE TABLE %s (key int PRIMARY KEY";
+        String insertDML = "INSERT INTO %s (key";
+        List<Object> values = new ArrayList<>();
+        for (TypesTestDef typeDef : typeDefs)
+        {
+            createTableDDL += ", " + typeDef.columnName + ' ' + typeDef.tableType;
+            insertDML += ", " + typeDef.columnName;
+            String typeName = typeDef.udfType;
+            typeDef.fCheckArgAndReturn = createFunction(KEYSPACE,
+                                                        typeName,
+                                                        "CREATE OR REPLACE FUNCTION %s(val
" + typeName + ") " +
+                                                        "CALLED ON NULL INPUT " +
+                                                        "RETURNS " + typeName + ' ' +
+                                                        "LANGUAGE java\n" +
+                                                        "AS 'return val;';");
+            typeDef.fCalledOnNull = createFunction(KEYSPACE,
+                                                   typeName,
+                                                   "CREATE OR REPLACE FUNCTION %s(val " +
typeName + ") " +
+                                                   "CALLED ON NULL INPUT " +
+                                                   "RETURNS text " +
+                                                   "LANGUAGE java\n" +
+                                                   "AS 'return \"called\";';");
+            typeDef.fReturnsNullOnNull = createFunction(KEYSPACE,
+                                                        typeName,
+                                                        "CREATE OR REPLACE FUNCTION %s(val
" + typeName + ") " +
+                                                        "RETURNS NULL ON NULL INPUT " +
+                                                        "RETURNS text " +
+                                                        "LANGUAGE java\n" +
+                                                        "AS 'return \"called\";';");
+            values.add(typeDef.referenceValue);
+        }
+
+        createTableDDL += ')';
+        createTable(createTableDDL);
+
+        insertDML += ") VALUES (1";
+        for (TypesTestDef ignored : typeDefs)
+            insertDML += ", ?";
+        insertDML += ')';
+
+        execute(insertDML, values.toArray());
+
+        // second row with null values
+        for (int i = 0; i < values.size(); i++)
+            values.set(i, null);
+        execute(insertDML.replace('1', '2'), values.toArray());
+
+        // check argument input + return
+        for (TypesTestDef typeDef : typeDefs)
+        {
+            assertRows(execute("SELECT " + typeDef.fCheckArgAndReturn + '(' + typeDef.columnName
+ ") FROM %s WHERE key = 1"),
+                       row(new Object[]{ typeDef.referenceValue }));
+        }
+
+        // check for CALLED ON NULL INPUT with non-null arguments
+        for (TypesTestDef typeDef : typeDefs)
+        {
+            assertRows(execute("SELECT " + typeDef.fCalledOnNull + '(' + typeDef.columnName
+ ") FROM %s WHERE key = 1"),
+                       row(new Object[]{ "called" }));
+        }
+
+        // check for CALLED ON NULL INPUT with null arguments
+        for (TypesTestDef typeDef : typeDefs)
+        {
+            assertRows(execute("SELECT " + typeDef.fCalledOnNull + '(' + typeDef.columnName
+ ") FROM %s WHERE key = 2"),
+                       row(new Object[]{ "called" }));
+        }
+
+        // check for RETURNS NULL ON NULL INPUT with non-null arguments
+        for (TypesTestDef typeDef : typeDefs)
+        {
+            assertRows(execute("SELECT " + typeDef.fReturnsNullOnNull + '(' + typeDef.columnName
+ ") FROM %s WHERE key = 1"),
+                       row(new Object[]{ "called" }));
+        }
+
+        // check for RETURNS NULL ON NULL INPUT with null arguments
+        for (TypesTestDef typeDef : typeDefs)
+        {
+            assertRows(execute("SELECT " + typeDef.fReturnsNullOnNull + '(' + typeDef.columnName
+ ") FROM %s WHERE key = 2"),
+                       row(new Object[]{ null }));
+        }
+
     }
 
     @Test
@@ -2051,81 +2147,6 @@ public class UFTest extends CQLTester
     }
 
     @Test
-    public void testNullOnCalledOnNullInput() throws Throwable
-    {
-        String type = createType("CREATE TYPE %s (txt text, i int)");
-        createTable("CREATE TABLE %s (key int PRIMARY KEY, i int, b bigint, f float, d double,
x boolean, t text, u frozen<"+type+">, tup frozen<tuple<int, text>>)");
-
-        execute("INSERT INTO %s (key, i, b, f, d, x, t, u, tup) VALUES (1, null, null, null,
null, null, null, null, null)");
-
-        String fI = createFunction(KEYSPACE,
-                                   "int",
-                                   "CREATE OR REPLACE FUNCTION %s(val int) " +
-                                   "CALLED ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fB = createFunction(KEYSPACE,
-                                   "bigint",
-                                   "CREATE OR REPLACE FUNCTION %s(val bigint) " +
-                                   "CALLED ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fF = createFunction(KEYSPACE,
-                                   "float",
-                                   "CREATE OR REPLACE FUNCTION %s(val float) " +
-                                   "CALLED ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fD = createFunction(KEYSPACE,
-                                   "double",
-                                   "CREATE OR REPLACE FUNCTION %s(val double) " +
-                                   "CALLED ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fX = createFunction(KEYSPACE,
-                                   "boolean",
-                                   "CREATE OR REPLACE FUNCTION %s(val boolean) " +
-                                   "CALLED ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fT = createFunction(KEYSPACE,
-                                   "text",
-                                   "CREATE OR REPLACE FUNCTION %s(val text) " +
-                                   "CALLED ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fU = createFunction(KEYSPACE,
-                                   type,
-                                   "CREATE OR REPLACE FUNCTION %s(val " + type + ") " +
-                                   "CALLED ON NULL INPUT " +
-                                   "RETURNS text " +
-                                   "LANGUAGE java\n" +
-                                   "AS 'return \"foo bar\";';");
-        String fTup = createFunction(KEYSPACE,
-                                     "tuple<int, text>",
-                                     "CREATE OR REPLACE FUNCTION %s(val tuple<int, text>)
" +
-                                     "CALLED ON NULL INPUT " +
-                                     "RETURNS text " +
-                                     "LANGUAGE java\n" +
-                                     "AS 'return \"foo bar\";';");
-
-        assertRows(execute("SELECT " + fI + "(i) FROM %s WHERE key=1"), row("foo bar"));
-        assertRows(execute("SELECT " + fB + "(b) FROM %s WHERE key=1"), row("foo bar"));
-        assertRows(execute("SELECT " + fF + "(f) FROM %s WHERE key=1"), row("foo bar"));
-        assertRows(execute("SELECT " + fD + "(d) FROM %s WHERE key=1"), row("foo bar"));
-        assertRows(execute("SELECT " + fX + "(x) FROM %s WHERE key=1"), row("foo bar"));
-        assertRows(execute("SELECT " + fT + "(t) FROM %s WHERE key=1"), row("foo bar"));
-        assertRows(execute("SELECT " + fU + "(u) FROM %s WHERE key=1"), row("foo bar"));
-        assertRows(execute("SELECT " + fTup + "(tup) FROM %s WHERE key=1"), row("foo bar"));
-    }
-
-    @Test
     public void testBrokenFunction() throws Throwable
     {
         createTable("CREATE TABLE %s (key int primary key, dval double)");


Mime
View raw message