AVRO-1684: Add time types to the specific compiler.

This adds a dependency on Joda time for both the compiler and the
compiled classes. When generating Java classes, any conversion that is
registered with the compiler's SpecificData instance will be used.

Closes #86.
diff --git a/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java b/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java
index 63fa025..e66726e 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java
@@ -74,8 +74,8 @@
     }
   }
 
-  private <T> Object convert(Schema schema, LogicalType logicalType,
-                             Conversion<T> conversion, Object datum) {
+  protected <T> Object convert(Schema schema, LogicalType logicalType,
+                               Conversion<T> conversion, Object datum) {
     if (conversion == null) {
       return datum;
     }
diff --git a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java
index 870d16f..774ca09 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java
@@ -17,10 +17,13 @@
  */
 package org.apache.avro.specific;
 
+import org.apache.avro.Conversion;
 import org.apache.avro.Schema;
 import org.apache.avro.AvroRuntimeException;
 import org.apache.avro.generic.GenericDatumReader;
+import org.apache.avro.io.ResolvingDecoder;
 import org.apache.avro.util.ClassUtils;
+import java.io.IOException;
 
 /** {@link org.apache.avro.io.DatumReader DatumReader} for generated Java classes. */
 public class SpecificDatumReader<T> extends GenericDatumReader<T> {
@@ -98,5 +101,26 @@
     }
   }
 
+  @Override
+  protected void readField(Object r, Schema.Field f, Object oldDatum,
+                           ResolvingDecoder in, Object state)
+      throws IOException {
+    if (r instanceof SpecificRecordBase) {
+      Conversion<?> conversion = ((SpecificRecordBase) r).getConversion(f.pos());
+
+      Object datum;
+      if (conversion != null) {
+        datum = readWithConversion(
+            oldDatum, f.schema(), f.schema().getLogicalType(), conversion, in);
+      } else {
+        datum = readWithoutConversion(oldDatum, f.schema(), in);
+      }
+
+      getData().setField(r, f.name(), f.pos(), datum);
+
+    } else {
+      super.readField(r, f, oldDatum, in, state);
+    }
+  }
 }
 
diff --git a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumWriter.java b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumWriter.java
index 128b02e..7bee02a 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumWriter.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumWriter.java
@@ -19,6 +19,8 @@
 
 import java.io.IOException;
 
+import org.apache.avro.Conversion;
+import org.apache.avro.LogicalType;
 import org.apache.avro.Schema;
 import org.apache.avro.generic.GenericDatumWriter;
 import org.apache.avro.io.Encoder;
@@ -69,5 +71,24 @@
     writeString(datum, out);
   }
 
+  @Override
+  protected void writeField(Object datum, Schema.Field f, Encoder out,
+                            Object state) throws IOException {
+    if (datum instanceof SpecificRecordBase) {
+      Conversion<?> conversion = ((SpecificRecordBase) datum).getConversion(f.pos());
+      Schema fieldSchema = f.schema();
+      LogicalType logicalType = fieldSchema.getLogicalType();
+
+      Object value = getData().getField(datum, f.name(), f.pos());
+      if (conversion != null && logicalType != null) {
+        value = convert(fieldSchema, logicalType, conversion, value);
+      }
+
+      writeWithoutConversion(fieldSchema, value, out);
+
+    } else {
+      super.writeField(datum, f, out, state);
+    }
+  }
 }
 
diff --git a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificRecordBase.java b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificRecordBase.java
index 77d0928..baedeb8 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificRecordBase.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificRecordBase.java
@@ -22,6 +22,7 @@
 import java.io.ObjectInput;
 import java.io.IOException;
 
+import org.apache.avro.Conversion;
 import org.apache.avro.Schema;
 import org.apache.avro.generic.GenericRecord;
 
@@ -34,6 +35,11 @@
   public abstract Object get(int field);
   public abstract void put(int field, Object value);
 
+  public Conversion<?> getConversion(int field) {
+    // for backward-compatibility. no older specific classes have conversions.
+    return null;
+  }
+
   @Override
   public void put(String fieldName, Object value) {
     put(getSchema().getField(fieldName).pos(), value);
@@ -44,6 +50,10 @@
     return get(getSchema().getField(fieldName).pos());
   }
 
+  public Conversion<?> getConverion(String fieldName) {
+    return getConversion(getSchema().getField(fieldName).pos());
+  }
+
   @Override
   public boolean equals(Object that) {
     if (that == this) return true;                        // identical object
diff --git a/lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithLogicalTypes.java b/lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithLogicalTypes.java
new file mode 100644
index 0000000..a01e450
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithLogicalTypes.java
@@ -0,0 +1,697 @@
+/**
+ * Autogenerated by Avro
+ * 
+ * DO NOT EDIT DIRECTLY
+ */
+package org.apache.avro.specific;
+@SuppressWarnings("all")
+@org.apache.avro.specific.AvroGenerated
+public class TestRecordWithLogicalTypes extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
+  private static final long serialVersionUID = -4211233492739285532L;
+  public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"TestRecordWithLogicalTypes\",\"namespace\":\"org.apache.avro.specific\",\"fields\":[{\"name\":\"b\",\"type\":\"boolean\"},{\"name\":\"i32\",\"type\":\"int\"},{\"name\":\"i64\",\"type\":\"long\"},{\"name\":\"f32\",\"type\":\"float\"},{\"name\":\"f64\",\"type\":\"double\"},{\"name\":\"s\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"d\",\"type\":{\"type\":\"int\",\"logicalType\":\"date\"}},{\"name\":\"t\",\"type\":{\"type\":\"int\",\"logicalType\":\"time-millis\"}},{\"name\":\"ts\",\"type\":{\"type\":\"long\",\"logicalType\":\"timestamp-millis\"}}]}");
+  public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; }
+  @Deprecated public boolean b;
+  @Deprecated public int i32;
+  @Deprecated public long i64;
+  @Deprecated public float f32;
+  @Deprecated public double f64;
+  @Deprecated public java.lang.CharSequence s;
+  @Deprecated public org.joda.time.LocalDate d;
+  @Deprecated public org.joda.time.LocalTime t;
+  @Deprecated public org.joda.time.DateTime ts;
+
+  /**
+   * Default constructor.  Note that this does not initialize fields
+   * to their default values from the schema.  If that is desired then
+   * one should use <code>newBuilder()</code>. 
+   */
+  public TestRecordWithLogicalTypes() {}
+
+  /**
+   * All-args constructor.
+   */
+  public TestRecordWithLogicalTypes(java.lang.Boolean b, java.lang.Integer i32, java.lang.Long i64, java.lang.Float f32, java.lang.Double f64, java.lang.CharSequence s, org.joda.time.LocalDate d, org.joda.time.LocalTime t, org.joda.time.DateTime ts) {
+    this.b = b;
+    this.i32 = i32;
+    this.i64 = i64;
+    this.f32 = f32;
+    this.f64 = f64;
+    this.s = s;
+    this.d = d;
+    this.t = t;
+    this.ts = ts;
+  }
+
+  public org.apache.avro.Schema getSchema() { return SCHEMA$; }
+  // Used by DatumWriter.  Applications should not call. 
+  public java.lang.Object get(int field$) {
+    switch (field$) {
+    case 0: return b;
+    case 1: return i32;
+    case 2: return i64;
+    case 3: return f32;
+    case 4: return f64;
+    case 5: return s;
+    case 6: return d;
+    case 7: return t;
+    case 8: return ts;
+    default: throw new org.apache.avro.AvroRuntimeException("Bad index");
+    }
+  }
+  // Used by DatumReader.  Applications should not call. 
+  @SuppressWarnings(value="unchecked")
+  public void put(int field$, java.lang.Object value$) {
+    switch (field$) {
+    case 0: b = (java.lang.Boolean)value$; break;
+    case 1: i32 = (java.lang.Integer)value$; break;
+    case 2: i64 = (java.lang.Long)value$; break;
+    case 3: f32 = (java.lang.Float)value$; break;
+    case 4: f64 = (java.lang.Double)value$; break;
+    case 5: s = (java.lang.CharSequence)value$; break;
+    case 6: d = (org.joda.time.LocalDate)value$; break;
+    case 7: t = (org.joda.time.LocalTime)value$; break;
+    case 8: ts = (org.joda.time.DateTime)value$; break;
+    default: throw new org.apache.avro.AvroRuntimeException("Bad index");
+    }
+  }
+
+  /**
+   * Gets the value of the 'b' field.
+   */
+  public java.lang.Boolean getB() {
+    return b;
+  }
+
+  /**
+   * Sets the value of the 'b' field.
+   * @param value the value to set.
+   */
+  public void setB(java.lang.Boolean value) {
+    this.b = value;
+  }
+
+  /**
+   * Gets the value of the 'i32' field.
+   */
+  public java.lang.Integer getI32() {
+    return i32;
+  }
+
+  /**
+   * Sets the value of the 'i32' field.
+   * @param value the value to set.
+   */
+  public void setI32(java.lang.Integer value) {
+    this.i32 = value;
+  }
+
+  /**
+   * Gets the value of the 'i64' field.
+   */
+  public java.lang.Long getI64() {
+    return i64;
+  }
+
+  /**
+   * Sets the value of the 'i64' field.
+   * @param value the value to set.
+   */
+  public void setI64(java.lang.Long value) {
+    this.i64 = value;
+  }
+
+  /**
+   * Gets the value of the 'f32' field.
+   */
+  public java.lang.Float getF32() {
+    return f32;
+  }
+
+  /**
+   * Sets the value of the 'f32' field.
+   * @param value the value to set.
+   */
+  public void setF32(java.lang.Float value) {
+    this.f32 = value;
+  }
+
+  /**
+   * Gets the value of the 'f64' field.
+   */
+  public java.lang.Double getF64() {
+    return f64;
+  }
+
+  /**
+   * Sets the value of the 'f64' field.
+   * @param value the value to set.
+   */
+  public void setF64(java.lang.Double value) {
+    this.f64 = value;
+  }
+
+  /**
+   * Gets the value of the 's' field.
+   */
+  public java.lang.CharSequence getS() {
+    return s;
+  }
+
+  /**
+   * Sets the value of the 's' field.
+   * @param value the value to set.
+   */
+  public void setS(java.lang.CharSequence value) {
+    this.s = value;
+  }
+
+  /**
+   * Gets the value of the 'd' field.
+   */
+  public org.joda.time.LocalDate getD() {
+    return d;
+  }
+
+  /**
+   * Sets the value of the 'd' field.
+   * @param value the value to set.
+   */
+  public void setD(org.joda.time.LocalDate value) {
+    this.d = value;
+  }
+
+  /**
+   * Gets the value of the 't' field.
+   */
+  public org.joda.time.LocalTime getT() {
+    return t;
+  }
+
+  /**
+   * Sets the value of the 't' field.
+   * @param value the value to set.
+   */
+  public void setT(org.joda.time.LocalTime value) {
+    this.t = value;
+  }
+
+  /**
+   * Gets the value of the 'ts' field.
+   */
+  public org.joda.time.DateTime getTs() {
+    return ts;
+  }
+
+  /**
+   * Sets the value of the 'ts' field.
+   * @param value the value to set.
+   */
+  public void setTs(org.joda.time.DateTime value) {
+    this.ts = value;
+  }
+
+  protected static final org.apache.avro.data.TimeConversions.DateConversion DATE_CONVERSION = new org.apache.avro.data.TimeConversions.DateConversion();
+  protected static final org.apache.avro.data.TimeConversions.TimeConversion TIME_CONVERSION = new org.apache.avro.data.TimeConversions.TimeConversion();
+  protected static final org.apache.avro.data.TimeConversions.TimestampConversion TIMESTAMP_CONVERSION = new org.apache.avro.data.TimeConversions.TimestampConversion();
+
+  private final org.apache.avro.Conversion<?>[] conversions =
+      new org.apache.avro.Conversion<?>[] {
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      DATE_CONVERSION,
+      TIME_CONVERSION,
+      TIMESTAMP_CONVERSION,
+      null
+  };
+
+  @Override
+  public org.apache.avro.Conversion<?> getConversion(int field) {
+    return conversions[field];
+  }
+
+  /** Creates a new TestRecordWithLogicalTypes RecordBuilder */
+  public static TestRecordWithLogicalTypes.Builder newBuilder() {
+    return new TestRecordWithLogicalTypes.Builder();
+  }
+  
+  /** Creates a new TestRecordWithLogicalTypes RecordBuilder by copying an existing Builder */
+  public static TestRecordWithLogicalTypes.Builder newBuilder(TestRecordWithLogicalTypes.Builder other) {
+    return new TestRecordWithLogicalTypes.Builder(other);
+  }
+  
+  /** Creates a new TestRecordWithLogicalTypes RecordBuilder by copying an existing TestRecordWithLogicalTypes instance */
+  public static TestRecordWithLogicalTypes.Builder newBuilder(TestRecordWithLogicalTypes other) {
+    return new TestRecordWithLogicalTypes.Builder(other);
+  }
+  
+  /**
+   * RecordBuilder for TestRecordWithLogicalTypes instances.
+   */
+  public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase<TestRecordWithLogicalTypes>
+    implements org.apache.avro.data.RecordBuilder<TestRecordWithLogicalTypes> {
+
+    private boolean b;
+    private int i32;
+    private long i64;
+    private float f32;
+    private double f64;
+    private java.lang.CharSequence s;
+    private org.joda.time.LocalDate d;
+    private org.joda.time.LocalTime t;
+    private org.joda.time.DateTime ts;
+
+    /** Creates a new Builder */
+    private Builder() {
+      super(TestRecordWithLogicalTypes.SCHEMA$);
+    }
+    
+    /** Creates a Builder by copying an existing Builder */
+    private Builder(TestRecordWithLogicalTypes.Builder other) {
+      super(other);
+      if (isValidValue(fields()[0], other.b)) {
+        this.b = data().deepCopy(fields()[0].schema(), other.b);
+        fieldSetFlags()[0] = true;
+      }
+      if (isValidValue(fields()[1], other.i32)) {
+        this.i32 = data().deepCopy(fields()[1].schema(), other.i32);
+        fieldSetFlags()[1] = true;
+      }
+      if (isValidValue(fields()[2], other.i64)) {
+        this.i64 = data().deepCopy(fields()[2].schema(), other.i64);
+        fieldSetFlags()[2] = true;
+      }
+      if (isValidValue(fields()[3], other.f32)) {
+        this.f32 = data().deepCopy(fields()[3].schema(), other.f32);
+        fieldSetFlags()[3] = true;
+      }
+      if (isValidValue(fields()[4], other.f64)) {
+        this.f64 = data().deepCopy(fields()[4].schema(), other.f64);
+        fieldSetFlags()[4] = true;
+      }
+      if (isValidValue(fields()[5], other.s)) {
+        this.s = data().deepCopy(fields()[5].schema(), other.s);
+        fieldSetFlags()[5] = true;
+      }
+      if (isValidValue(fields()[6], other.d)) {
+        this.d = data().deepCopy(fields()[6].schema(), other.d);
+        fieldSetFlags()[6] = true;
+      }
+      if (isValidValue(fields()[7], other.t)) {
+        this.t = data().deepCopy(fields()[7].schema(), other.t);
+        fieldSetFlags()[7] = true;
+      }
+      if (isValidValue(fields()[8], other.ts)) {
+        this.ts = data().deepCopy(fields()[8].schema(), other.ts);
+        fieldSetFlags()[8] = true;
+      }
+    }
+    
+    /** Creates a Builder by copying an existing TestRecordWithLogicalTypes instance */
+    private Builder(TestRecordWithLogicalTypes other) {
+            super(TestRecordWithLogicalTypes.SCHEMA$);
+      if (isValidValue(fields()[0], other.b)) {
+        this.b = data().deepCopy(fields()[0].schema(), other.b);
+        fieldSetFlags()[0] = true;
+      }
+      if (isValidValue(fields()[1], other.i32)) {
+        this.i32 = data().deepCopy(fields()[1].schema(), other.i32);
+        fieldSetFlags()[1] = true;
+      }
+      if (isValidValue(fields()[2], other.i64)) {
+        this.i64 = data().deepCopy(fields()[2].schema(), other.i64);
+        fieldSetFlags()[2] = true;
+      }
+      if (isValidValue(fields()[3], other.f32)) {
+        this.f32 = data().deepCopy(fields()[3].schema(), other.f32);
+        fieldSetFlags()[3] = true;
+      }
+      if (isValidValue(fields()[4], other.f64)) {
+        this.f64 = data().deepCopy(fields()[4].schema(), other.f64);
+        fieldSetFlags()[4] = true;
+      }
+      if (isValidValue(fields()[5], other.s)) {
+        this.s = data().deepCopy(fields()[5].schema(), other.s);
+        fieldSetFlags()[5] = true;
+      }
+      if (isValidValue(fields()[6], other.d)) {
+        this.d = data().deepCopy(fields()[6].schema(), other.d);
+        fieldSetFlags()[6] = true;
+      }
+      if (isValidValue(fields()[7], other.t)) {
+        this.t = data().deepCopy(fields()[7].schema(), other.t);
+        fieldSetFlags()[7] = true;
+      }
+      if (isValidValue(fields()[8], other.ts)) {
+        this.ts = data().deepCopy(fields()[8].schema(), other.ts);
+        fieldSetFlags()[8] = true;
+      }
+    }
+
+    /**
+      * Gets the value of the 'b' field.
+      */
+    public java.lang.Boolean getB() {
+      return b;
+    }
+
+    /**
+      * Sets the value of the 'b' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setB(boolean value) {
+      validate(fields()[0], value);
+      this.b = value;
+      fieldSetFlags()[0] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 'b' field has been set.
+      */
+    public boolean hasB() {
+      return fieldSetFlags()[0];
+    }
+
+
+    /**
+      * Clears the value of the 'b' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearB() {
+      fieldSetFlags()[0] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 'i32' field.
+      */
+    public java.lang.Integer getI32() {
+      return i32;
+    }
+
+    /**
+      * Sets the value of the 'i32' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setI32(int value) {
+      validate(fields()[1], value);
+      this.i32 = value;
+      fieldSetFlags()[1] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 'i32' field has been set.
+      */
+    public boolean hasI32() {
+      return fieldSetFlags()[1];
+    }
+
+
+    /**
+      * Clears the value of the 'i32' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearI32() {
+      fieldSetFlags()[1] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 'i64' field.
+      */
+    public java.lang.Long getI64() {
+      return i64;
+    }
+
+    /**
+      * Sets the value of the 'i64' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setI64(long value) {
+      validate(fields()[2], value);
+      this.i64 = value;
+      fieldSetFlags()[2] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 'i64' field has been set.
+      */
+    public boolean hasI64() {
+      return fieldSetFlags()[2];
+    }
+
+
+    /**
+      * Clears the value of the 'i64' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearI64() {
+      fieldSetFlags()[2] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 'f32' field.
+      */
+    public java.lang.Float getF32() {
+      return f32;
+    }
+
+    /**
+      * Sets the value of the 'f32' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setF32(float value) {
+      validate(fields()[3], value);
+      this.f32 = value;
+      fieldSetFlags()[3] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 'f32' field has been set.
+      */
+    public boolean hasF32() {
+      return fieldSetFlags()[3];
+    }
+
+
+    /**
+      * Clears the value of the 'f32' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearF32() {
+      fieldSetFlags()[3] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 'f64' field.
+      */
+    public java.lang.Double getF64() {
+      return f64;
+    }
+
+    /**
+      * Sets the value of the 'f64' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setF64(double value) {
+      validate(fields()[4], value);
+      this.f64 = value;
+      fieldSetFlags()[4] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 'f64' field has been set.
+      */
+    public boolean hasF64() {
+      return fieldSetFlags()[4];
+    }
+
+
+    /**
+      * Clears the value of the 'f64' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearF64() {
+      fieldSetFlags()[4] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 's' field.
+      */
+    public java.lang.CharSequence getS() {
+      return s;
+    }
+
+    /**
+      * Sets the value of the 's' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setS(java.lang.CharSequence value) {
+      validate(fields()[5], value);
+      this.s = value;
+      fieldSetFlags()[5] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 's' field has been set.
+      */
+    public boolean hasS() {
+      return fieldSetFlags()[5];
+    }
+
+
+    /**
+      * Clears the value of the 's' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearS() {
+      s = null;
+      fieldSetFlags()[5] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 'd' field.
+      */
+    public org.joda.time.LocalDate getD() {
+      return d;
+    }
+
+    /**
+      * Sets the value of the 'd' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setD(org.joda.time.LocalDate value) {
+      validate(fields()[6], value);
+      this.d = value;
+      fieldSetFlags()[6] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 'd' field has been set.
+      */
+    public boolean hasD() {
+      return fieldSetFlags()[6];
+    }
+
+
+    /**
+      * Clears the value of the 'd' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearD() {
+      fieldSetFlags()[6] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 't' field.
+      */
+    public org.joda.time.LocalTime getT() {
+      return t;
+    }
+
+    /**
+      * Sets the value of the 't' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setT(org.joda.time.LocalTime value) {
+      validate(fields()[7], value);
+      this.t = value;
+      fieldSetFlags()[7] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 't' field has been set.
+      */
+    public boolean hasT() {
+      return fieldSetFlags()[7];
+    }
+
+
+    /**
+      * Clears the value of the 't' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearT() {
+      fieldSetFlags()[7] = false;
+      return this;
+    }
+
+    /**
+      * Gets the value of the 'ts' field.
+      */
+    public org.joda.time.DateTime getTs() {
+      return ts;
+    }
+
+    /**
+      * Sets the value of the 'ts' field.
+      * @param value the value to set.
+      */
+    public TestRecordWithLogicalTypes.Builder setTs(org.joda.time.DateTime value) {
+      validate(fields()[8], value);
+      this.ts = value;
+      fieldSetFlags()[8] = true;
+      return this; 
+    }
+
+    /**
+      * Checks whether the 'ts' field has been set.
+      */
+    public boolean hasTs() {
+      return fieldSetFlags()[8];
+    }
+
+
+    /**
+      * Clears the value of the 'ts' field.
+      */
+    public TestRecordWithLogicalTypes.Builder clearTs() {
+      fieldSetFlags()[8] = false;
+      return this;
+    }
+
+    @Override
+    public TestRecordWithLogicalTypes build() {
+      try {
+        TestRecordWithLogicalTypes record = new TestRecordWithLogicalTypes();
+        record.b = fieldSetFlags()[0] ? this.b : (java.lang.Boolean) defaultValue(fields()[0]);
+        record.i32 = fieldSetFlags()[1] ? this.i32 : (java.lang.Integer) defaultValue(fields()[1]);
+        record.i64 = fieldSetFlags()[2] ? this.i64 : (java.lang.Long) defaultValue(fields()[2]);
+        record.f32 = fieldSetFlags()[3] ? this.f32 : (java.lang.Float) defaultValue(fields()[3]);
+        record.f64 = fieldSetFlags()[4] ? this.f64 : (java.lang.Double) defaultValue(fields()[4]);
+        record.s = fieldSetFlags()[5] ? this.s : (java.lang.CharSequence) defaultValue(fields()[5]);
+        record.d = fieldSetFlags()[6] ? this.d : (org.joda.time.LocalDate) defaultValue(fields()[6]);
+        record.t = fieldSetFlags()[7] ? this.t : (org.joda.time.LocalTime) defaultValue(fields()[7]);
+        record.ts = fieldSetFlags()[8] ? this.ts : (org.joda.time.DateTime) defaultValue(fields()[8]);
+        return record;
+      } catch (Exception e) {
+        throw new org.apache.avro.AvroRuntimeException(e);
+      }
+    }
+  }
+
+  private static final org.apache.avro.io.DatumWriter
+    WRITER$ = new org.apache.avro.specific.SpecificDatumWriter(SCHEMA$);  
+
+  @Override public void writeExternal(java.io.ObjectOutput out)
+    throws java.io.IOException {
+    WRITER$.write(this, org.apache.avro.specific.SpecificData.getEncoder(out));
+  }
+
+  private static final org.apache.avro.io.DatumReader
+    READER$ = new org.apache.avro.specific.SpecificDatumReader(SCHEMA$);  
+
+  @Override public void readExternal(java.io.ObjectInput in)
+    throws java.io.IOException {
+    READER$.read(this, org.apache.avro.specific.SpecificData.getDecoder(in));
+  }
+
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithoutLogicalTypes.java b/lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithoutLogicalTypes.java
new file mode 100644
index 0000000..afe7d11
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/specific/TestRecordWithoutLogicalTypes.java
@@ -0,0 +1,503 @@
+/**
+ * Autogenerated by Avro
+ * 
+ * DO NOT EDIT DIRECTLY
+ */
+package org.apache.avro.specific;  
+@SuppressWarnings("all")
+@org.apache.avro.specific.AvroGenerated
+public class TestRecordWithoutLogicalTypes extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
+  public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"TestRecordWithoutLogicalTypes\",\"namespace\":\"org.apache.avro.specific\",\"fields\":[{\"name\":\"b\",\"type\":\"boolean\"},{\"name\":\"i32\",\"type\":\"int\"},{\"name\":\"i64\",\"type\":\"long\"},{\"name\":\"f32\",\"type\":\"float\"},{\"name\":\"f64\",\"type\":\"double\"},{\"name\":\"s\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"d\",\"type\":{\"type\":\"int\",\"logicalType\":\"date\"}},{\"name\":\"t\",\"type\":{\"type\":\"int\",\"logicalType\":\"time-millis\"}},{\"name\":\"ts\",\"type\":{\"type\":\"long\",\"logicalType\":\"timestamp-millis\"}}]}");
+  public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; }
+   private boolean b;
+   private int i32;
+   private long i64;
+   private float f32;
+   private double f64;
+   private java.lang.String s;
+   private int d;
+   private int t;
+   private long ts;
+
+  /**
+   * Default constructor.  Note that this does not initialize fields
+   * to their default values from the schema.  If that is desired then
+   * one should use {@link \#newBuilder()}. 
+   */
+  public TestRecordWithoutLogicalTypes() {}
+
+  /**
+   * All-args constructor.
+   */
+  public TestRecordWithoutLogicalTypes(java.lang.Boolean b, java.lang.Integer i32, java.lang.Long i64, java.lang.Float f32, java.lang.Double f64, java.lang.String s, java.lang.Integer d, java.lang.Integer t, java.lang.Long ts) {
+    this.b = b;
+    this.i32 = i32;
+    this.i64 = i64;
+    this.f32 = f32;
+    this.f64 = f64;
+    this.s = s;
+    this.d = d;
+    this.t = t;
+    this.ts = ts;
+  }
+
+  public org.apache.avro.Schema getSchema() { return SCHEMA$; }
+  // Used by DatumWriter.  Applications should not call. 
+  public java.lang.Object get(int field$) {
+    switch (field$) {
+    case 0: return b;
+    case 1: return i32;
+    case 2: return i64;
+    case 3: return f32;
+    case 4: return f64;
+    case 5: return s;
+    case 6: return d;
+    case 7: return t;
+    case 8: return ts;
+    default: throw new org.apache.avro.AvroRuntimeException("Bad index");
+    }
+  }
+  // Used by DatumReader.  Applications should not call. 
+  @SuppressWarnings(value="unchecked")
+  public void put(int field$, java.lang.Object value$) {
+    switch (field$) {
+    case 0: b = (java.lang.Boolean)value$; break;
+    case 1: i32 = (java.lang.Integer)value$; break;
+    case 2: i64 = (java.lang.Long)value$; break;
+    case 3: f32 = (java.lang.Float)value$; break;
+    case 4: f64 = (java.lang.Double)value$; break;
+    case 5: s = (java.lang.String)value$; break;
+    case 6: d = (java.lang.Integer)value$; break;
+    case 7: t = (java.lang.Integer)value$; break;
+    case 8: ts = (java.lang.Long)value$; break;
+    default: throw new org.apache.avro.AvroRuntimeException("Bad index");
+    }
+  }
+
+  /**
+   * Gets the value of the 'b' field.
+   */
+  public java.lang.Boolean getB() {
+    return b;
+  }
+
+
+  /**
+   * Gets the value of the 'i32' field.
+   */
+  public java.lang.Integer getI32() {
+    return i32;
+  }
+
+
+  /**
+   * Gets the value of the 'i64' field.
+   */
+  public java.lang.Long getI64() {
+    return i64;
+  }
+
+
+  /**
+   * Gets the value of the 'f32' field.
+   */
+  public java.lang.Float getF32() {
+    return f32;
+  }
+
+
+  /**
+   * Gets the value of the 'f64' field.
+   */
+  public java.lang.Double getF64() {
+    return f64;
+  }
+
+
+  /**
+   * Gets the value of the 's' field.
+   */
+  public java.lang.String getS() {
+    return s;
+  }
+
+
+  /**
+   * Gets the value of the 'd' field.
+   */
+  public java.lang.Integer getD() {
+    return d;
+  }
+
+
+  /**
+   * Gets the value of the 't' field.
+   */
+  public java.lang.Integer getT() {
+    return t;
+  }
+
+
+  /**
+   * Gets the value of the 'ts' field.
+   */
+  public java.lang.Long getTs() {
+    return ts;
+  }
+
+
+  /** Creates a new TestRecordWithoutLogicalTypes RecordBuilder */
+  public static TestRecordWithoutLogicalTypes.Builder newBuilder() {
+    return new TestRecordWithoutLogicalTypes.Builder();
+  }
+  
+  /** Creates a new TestRecordWithoutLogicalTypes RecordBuilder by copying an existing Builder */
+  public static TestRecordWithoutLogicalTypes.Builder newBuilder(TestRecordWithoutLogicalTypes.Builder other) {
+    return new TestRecordWithoutLogicalTypes.Builder(other);
+  }
+  
+  /** Creates a new TestRecordWithoutLogicalTypes RecordBuilder by copying an existing TestRecordWithoutLogicalTypes instance */
+  public static TestRecordWithoutLogicalTypes.Builder newBuilder(TestRecordWithoutLogicalTypes other) {
+    return new TestRecordWithoutLogicalTypes.Builder(other);
+  }
+  
+  /**
+   * RecordBuilder for TestRecordWithoutLogicalTypes instances.
+   */
+  public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase<TestRecordWithoutLogicalTypes>
+    implements org.apache.avro.data.RecordBuilder<TestRecordWithoutLogicalTypes> {
+
+    private boolean b;
+    private int i32;
+    private long i64;
+    private float f32;
+    private double f64;
+    private java.lang.String s;
+    private int d;
+    private int t;
+    private long ts;
+
+    /** Creates a new Builder */
+    private Builder() {
+      super(TestRecordWithoutLogicalTypes.SCHEMA$);
+    }
+    
+    /** Creates a Builder by copying an existing Builder */
+    private Builder(TestRecordWithoutLogicalTypes.Builder other) {
+      super(other);
+      if (isValidValue(fields()[0], other.b)) {
+        this.b = data().deepCopy(fields()[0].schema(), other.b);
+        fieldSetFlags()[0] = true;
+      }
+      if (isValidValue(fields()[1], other.i32)) {
+        this.i32 = data().deepCopy(fields()[1].schema(), other.i32);
+        fieldSetFlags()[1] = true;
+      }
+      if (isValidValue(fields()[2], other.i64)) {
+        this.i64 = data().deepCopy(fields()[2].schema(), other.i64);
+        fieldSetFlags()[2] = true;
+      }
+      if (isValidValue(fields()[3], other.f32)) {
+        this.f32 = data().deepCopy(fields()[3].schema(), other.f32);
+        fieldSetFlags()[3] = true;
+      }
+      if (isValidValue(fields()[4], other.f64)) {
+        this.f64 = data().deepCopy(fields()[4].schema(), other.f64);
+        fieldSetFlags()[4] = true;
+      }
+      if (isValidValue(fields()[5], other.s)) {
+        this.s = data().deepCopy(fields()[5].schema(), other.s);
+        fieldSetFlags()[5] = true;
+      }
+      if (isValidValue(fields()[6], other.d)) {
+        this.d = data().deepCopy(fields()[6].schema(), other.d);
+        fieldSetFlags()[6] = true;
+      }
+      if (isValidValue(fields()[7], other.t)) {
+        this.t = data().deepCopy(fields()[7].schema(), other.t);
+        fieldSetFlags()[7] = true;
+      }
+      if (isValidValue(fields()[8], other.ts)) {
+        this.ts = data().deepCopy(fields()[8].schema(), other.ts);
+        fieldSetFlags()[8] = true;
+      }
+    }
+    
+    /** Creates a Builder by copying an existing TestRecordWithoutLogicalTypes instance */
+    private Builder(TestRecordWithoutLogicalTypes other) {
+            super(TestRecordWithoutLogicalTypes.SCHEMA$);
+      if (isValidValue(fields()[0], other.b)) {
+        this.b = data().deepCopy(fields()[0].schema(), other.b);
+        fieldSetFlags()[0] = true;
+      }
+      if (isValidValue(fields()[1], other.i32)) {
+        this.i32 = data().deepCopy(fields()[1].schema(), other.i32);
+        fieldSetFlags()[1] = true;
+      }
+      if (isValidValue(fields()[2], other.i64)) {
+        this.i64 = data().deepCopy(fields()[2].schema(), other.i64);
+        fieldSetFlags()[2] = true;
+      }
+      if (isValidValue(fields()[3], other.f32)) {
+        this.f32 = data().deepCopy(fields()[3].schema(), other.f32);
+        fieldSetFlags()[3] = true;
+      }
+      if (isValidValue(fields()[4], other.f64)) {
+        this.f64 = data().deepCopy(fields()[4].schema(), other.f64);
+        fieldSetFlags()[4] = true;
+      }
+      if (isValidValue(fields()[5], other.s)) {
+        this.s = data().deepCopy(fields()[5].schema(), other.s);
+        fieldSetFlags()[5] = true;
+      }
+      if (isValidValue(fields()[6], other.d)) {
+        this.d = data().deepCopy(fields()[6].schema(), other.d);
+        fieldSetFlags()[6] = true;
+      }
+      if (isValidValue(fields()[7], other.t)) {
+        this.t = data().deepCopy(fields()[7].schema(), other.t);
+        fieldSetFlags()[7] = true;
+      }
+      if (isValidValue(fields()[8], other.ts)) {
+        this.ts = data().deepCopy(fields()[8].schema(), other.ts);
+        fieldSetFlags()[8] = true;
+      }
+    }
+
+    /** Gets the value of the 'b' field */
+    public java.lang.Boolean getB() {
+      return b;
+    }
+    
+    /** Sets the value of the 'b' field */
+    public TestRecordWithoutLogicalTypes.Builder setB(boolean value) {
+      validate(fields()[0], value);
+      this.b = value;
+      fieldSetFlags()[0] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 'b' field has been set */
+    public boolean hasB() {
+      return fieldSetFlags()[0];
+    }
+    
+    /** Clears the value of the 'b' field */
+    public TestRecordWithoutLogicalTypes.Builder clearB() {
+      fieldSetFlags()[0] = false;
+      return this;
+    }
+
+    /** Gets the value of the 'i32' field */
+    public java.lang.Integer getI32() {
+      return i32;
+    }
+    
+    /** Sets the value of the 'i32' field */
+    public TestRecordWithoutLogicalTypes.Builder setI32(int value) {
+      validate(fields()[1], value);
+      this.i32 = value;
+      fieldSetFlags()[1] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 'i32' field has been set */
+    public boolean hasI32() {
+      return fieldSetFlags()[1];
+    }
+    
+    /** Clears the value of the 'i32' field */
+    public TestRecordWithoutLogicalTypes.Builder clearI32() {
+      fieldSetFlags()[1] = false;
+      return this;
+    }
+
+    /** Gets the value of the 'i64' field */
+    public java.lang.Long getI64() {
+      return i64;
+    }
+    
+    /** Sets the value of the 'i64' field */
+    public TestRecordWithoutLogicalTypes.Builder setI64(long value) {
+      validate(fields()[2], value);
+      this.i64 = value;
+      fieldSetFlags()[2] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 'i64' field has been set */
+    public boolean hasI64() {
+      return fieldSetFlags()[2];
+    }
+    
+    /** Clears the value of the 'i64' field */
+    public TestRecordWithoutLogicalTypes.Builder clearI64() {
+      fieldSetFlags()[2] = false;
+      return this;
+    }
+
+    /** Gets the value of the 'f32' field */
+    public java.lang.Float getF32() {
+      return f32;
+    }
+    
+    /** Sets the value of the 'f32' field */
+    public TestRecordWithoutLogicalTypes.Builder setF32(float value) {
+      validate(fields()[3], value);
+      this.f32 = value;
+      fieldSetFlags()[3] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 'f32' field has been set */
+    public boolean hasF32() {
+      return fieldSetFlags()[3];
+    }
+    
+    /** Clears the value of the 'f32' field */
+    public TestRecordWithoutLogicalTypes.Builder clearF32() {
+      fieldSetFlags()[3] = false;
+      return this;
+    }
+
+    /** Gets the value of the 'f64' field */
+    public java.lang.Double getF64() {
+      return f64;
+    }
+    
+    /** Sets the value of the 'f64' field */
+    public TestRecordWithoutLogicalTypes.Builder setF64(double value) {
+      validate(fields()[4], value);
+      this.f64 = value;
+      fieldSetFlags()[4] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 'f64' field has been set */
+    public boolean hasF64() {
+      return fieldSetFlags()[4];
+    }
+    
+    /** Clears the value of the 'f64' field */
+    public TestRecordWithoutLogicalTypes.Builder clearF64() {
+      fieldSetFlags()[4] = false;
+      return this;
+    }
+
+    /** Gets the value of the 's' field */
+    public java.lang.String getS() {
+      return s;
+    }
+    
+    /** Sets the value of the 's' field */
+    public TestRecordWithoutLogicalTypes.Builder setS(java.lang.String value) {
+      validate(fields()[5], value);
+      this.s = value;
+      fieldSetFlags()[5] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 's' field has been set */
+    public boolean hasS() {
+      return fieldSetFlags()[5];
+    }
+    
+    /** Clears the value of the 's' field */
+    public TestRecordWithoutLogicalTypes.Builder clearS() {
+      s = null;
+      fieldSetFlags()[5] = false;
+      return this;
+    }
+
+    /** Gets the value of the 'd' field */
+    public java.lang.Integer getD() {
+      return d;
+    }
+    
+    /** Sets the value of the 'd' field */
+    public TestRecordWithoutLogicalTypes.Builder setD(int value) {
+      validate(fields()[6], value);
+      this.d = value;
+      fieldSetFlags()[6] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 'd' field has been set */
+    public boolean hasD() {
+      return fieldSetFlags()[6];
+    }
+    
+    /** Clears the value of the 'd' field */
+    public TestRecordWithoutLogicalTypes.Builder clearD() {
+      fieldSetFlags()[6] = false;
+      return this;
+    }
+
+    /** Gets the value of the 't' field */
+    public java.lang.Integer getT() {
+      return t;
+    }
+    
+    /** Sets the value of the 't' field */
+    public TestRecordWithoutLogicalTypes.Builder setT(int value) {
+      validate(fields()[7], value);
+      this.t = value;
+      fieldSetFlags()[7] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 't' field has been set */
+    public boolean hasT() {
+      return fieldSetFlags()[7];
+    }
+    
+    /** Clears the value of the 't' field */
+    public TestRecordWithoutLogicalTypes.Builder clearT() {
+      fieldSetFlags()[7] = false;
+      return this;
+    }
+
+    /** Gets the value of the 'ts' field */
+    public java.lang.Long getTs() {
+      return ts;
+    }
+    
+    /** Sets the value of the 'ts' field */
+    public TestRecordWithoutLogicalTypes.Builder setTs(long value) {
+      validate(fields()[8], value);
+      this.ts = value;
+      fieldSetFlags()[8] = true;
+      return this; 
+    }
+    
+    /** Checks whether the 'ts' field has been set */
+    public boolean hasTs() {
+      return fieldSetFlags()[8];
+    }
+    
+    /** Clears the value of the 'ts' field */
+    public TestRecordWithoutLogicalTypes.Builder clearTs() {
+      fieldSetFlags()[8] = false;
+      return this;
+    }
+
+    @Override
+    public TestRecordWithoutLogicalTypes build() {
+      try {
+        TestRecordWithoutLogicalTypes record = new TestRecordWithoutLogicalTypes();
+        record.b = fieldSetFlags()[0] ? this.b : (java.lang.Boolean) defaultValue(fields()[0]);
+        record.i32 = fieldSetFlags()[1] ? this.i32 : (java.lang.Integer) defaultValue(fields()[1]);
+        record.i64 = fieldSetFlags()[2] ? this.i64 : (java.lang.Long) defaultValue(fields()[2]);
+        record.f32 = fieldSetFlags()[3] ? this.f32 : (java.lang.Float) defaultValue(fields()[3]);
+        record.f64 = fieldSetFlags()[4] ? this.f64 : (java.lang.Double) defaultValue(fields()[4]);
+        record.s = fieldSetFlags()[5] ? this.s : (java.lang.String) defaultValue(fields()[5]);
+        record.d = fieldSetFlags()[6] ? this.d : (java.lang.Integer) defaultValue(fields()[6]);
+        record.t = fieldSetFlags()[7] ? this.t : (java.lang.Integer) defaultValue(fields()[7]);
+        record.ts = fieldSetFlags()[8] ? this.ts : (java.lang.Long) defaultValue(fields()[8]);
+        return record;
+      } catch (Exception e) {
+        throw new org.apache.avro.AvroRuntimeException(e);
+      }
+    }
+  }
+}
diff --git a/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificLogicalTypes.java b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificLogicalTypes.java
new file mode 100644
index 0000000..c545c5a
--- /dev/null
+++ b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificLogicalTypes.java
@@ -0,0 +1,210 @@
+package org.apache.avro.specific;
+
+import org.apache.avro.Schema;
+import org.apache.avro.data.TimeConversions.DateConversion;
+import org.apache.avro.data.TimeConversions.TimeConversion;
+import org.apache.avro.data.TimeConversions.TimestampConversion;
+import org.apache.avro.file.DataFileReader;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.file.FileReader;
+import org.apache.avro.io.DatumReader;
+import org.apache.avro.io.DatumWriter;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+import org.joda.time.LocalTime;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This tests compatibility between classes generated before and after
+ * AVRO-1684. TestRecordWithoutLogicalTypes and TestRecordWithLogicalTypes were
+ * generated from the same schema, found in
+ * src/test/resources/record_with_logical_types.avsc, and
+ * TestRecordWithoutLogicalTypes was renamed to avoid the conflict.
+ *
+ * The classes should not be re-generated because they test compatibility of
+ * Avro with existing Avro-generated sources. When using classes generated
+ * before AVRO-1684, logical types should not be applied by the read or write
+ * paths. Those files should behave as they did before.
+ */
+public class TestSpecificLogicalTypes {
+
+  @Rule
+  public final TemporaryFolder temp = new TemporaryFolder();
+
+  @Test
+  public void testRecordWithLogicalTypes() throws IOException {
+    TestRecordWithLogicalTypes record = new TestRecordWithLogicalTypes(
+        true,
+        34,
+        35L,
+        3.14F,
+        3019.34,
+        null,
+        LocalDate.now(),
+        LocalTime.now(),
+        DateTime.now().withZone(DateTimeZone.UTC)
+    );
+
+    File data = write(TestRecordWithLogicalTypes.getClassSchema(), record);
+    List<TestRecordWithLogicalTypes> actual = read(
+        TestRecordWithLogicalTypes.getClassSchema(), data);
+
+    Assert.assertEquals("Should match written record", record, actual.get(0));
+  }
+
+  @Test
+  public void testRecordWithoutLogicalTypes() throws IOException {
+    // the significance of the record without logical types is that it has the
+    // same schema (besides record name) as the one with logical types,
+    // including the type annotations. this verifies that the type annotations
+    // are only applied if the record was compiled to use those types. this
+    // ensures compatibility with already-compiled code.
+
+    TestRecordWithoutLogicalTypes record = new TestRecordWithoutLogicalTypes(
+        true,
+        34,
+        35L,
+        3.14F,
+        3019.34,
+        null,
+        new DateConversion().toInt(LocalDate.now(), null, null),
+        new TimeConversion().toInt(LocalTime.now(), null, null),
+        new TimestampConversion().toLong(
+            DateTime.now().withZone(DateTimeZone.UTC), null, null)
+    );
+
+    File data = write(TestRecordWithoutLogicalTypes.getClassSchema(), record);
+    List<TestRecordWithoutLogicalTypes> actual = read(
+        TestRecordWithoutLogicalTypes.getClassSchema(), data);
+
+    Assert.assertEquals("Should match written record", record, actual.get(0));
+  }
+
+  @Test
+  public void testRecordWritePrimitivesReadLogicalTypes() throws IOException {
+    LocalDate date = LocalDate.now();
+    LocalTime time = LocalTime.now();
+    DateTime timestamp = DateTime.now().withZone(DateTimeZone.UTC);
+
+    TestRecordWithoutLogicalTypes record = new TestRecordWithoutLogicalTypes(
+        true,
+        34,
+        35L,
+        3.14F,
+        3019.34,
+        null,
+        new DateConversion().toInt(date, null, null),
+        new TimeConversion().toInt(time, null, null),
+        new TimestampConversion().toLong(timestamp, null, null)
+    );
+
+    File data = write(TestRecordWithoutLogicalTypes.getClassSchema(), record);
+    // read using the schema with logical types
+    List<TestRecordWithLogicalTypes> actual = read(
+        TestRecordWithLogicalTypes.getClassSchema(), data);
+
+    TestRecordWithLogicalTypes expected = new TestRecordWithLogicalTypes(
+        true,
+        34,
+        35L,
+        3.14F,
+        3019.34,
+        null,
+        date,
+        time,
+        timestamp
+    );
+
+    Assert.assertEquals("Should match written record", expected, actual.get(0));
+  }
+
+  @Test
+  public void testRecordWriteLogicalTypesReadPrimitives() throws IOException {
+    LocalDate date = LocalDate.now();
+    LocalTime time = LocalTime.now();
+    DateTime timestamp = DateTime.now().withZone(DateTimeZone.UTC);
+
+    TestRecordWithLogicalTypes record = new TestRecordWithLogicalTypes(
+        true,
+        34,
+        35L,
+        3.14F,
+        3019.34,
+        null,
+        date,
+        time,
+        timestamp
+    );
+
+    File data = write(TestRecordWithLogicalTypes.getClassSchema(), record);
+    // read using the schema with logical types
+    List<TestRecordWithoutLogicalTypes> actual = read(
+        TestRecordWithoutLogicalTypes.getClassSchema(), data);
+
+    TestRecordWithoutLogicalTypes expected = new TestRecordWithoutLogicalTypes(
+        true,
+        34,
+        35L,
+        3.14F,
+        3019.34,
+        null,
+        new DateConversion().toInt(date, null, null),
+        new TimeConversion().toInt(time, null, null),
+        new TimestampConversion().toLong(timestamp, null, null)
+    );
+
+    Assert.assertEquals("Should match written record", expected, actual.get(0));
+  }
+
+  private <D> List<D> read(Schema schema, File file)
+      throws IOException {
+    DatumReader<D> reader = newReader(schema);
+    List<D> data = new ArrayList<D>();
+    FileReader<D> fileReader = null;
+
+    try {
+      fileReader = new DataFileReader<D>(file, reader);
+      for (D datum : fileReader) {
+        data.add(datum);
+      }
+    } finally {
+      if (fileReader != null) {
+        fileReader.close();
+      }
+    }
+
+    return data;
+  }
+
+  @SuppressWarnings("unchecked")
+  private <D> DatumReader<D> newReader(Schema schema) {
+    return SpecificData.get().createDatumReader(schema);
+  }
+
+  @SuppressWarnings("unchecked")
+  private <D extends SpecificRecord> File write(Schema schema, D... data)
+      throws IOException {
+    File file = temp.newFile();
+    DatumWriter<D> writer = SpecificData.get().createDatumWriter(schema);
+    DataFileWriter<D> fileWriter = new DataFileWriter<D>(writer);
+
+    try {
+      fileWriter.create(schema, file);
+      for (D datum : data) {
+        fileWriter.append(datum);
+      }
+    } finally {
+      fileWriter.close();
+    }
+
+    return file;
+  }
+}
diff --git a/lang/java/avro/src/test/resources/record_with_logical_types.avsc b/lang/java/avro/src/test/resources/record_with_logical_types.avsc
new file mode 100644
index 0000000..9932f95
--- /dev/null
+++ b/lang/java/avro/src/test/resources/record_with_logical_types.avsc
@@ -0,0 +1,45 @@
+{
+  "type" : "record",
+  "name" : "TestRecordWithLogicalTypes",
+  "doc" : "Schema for TestRecordWithLogicalTypes and TestRecordWithoutLogicalTypes, see TestSpecificLogicalTypes"
+  "namespace" : "org.apache.avro.specific",
+  "fields" : [ {
+    "name" : "b",
+    "type" : "boolean"
+  }, {
+    "name" : "i32",
+    "type" : "int"
+  }, {
+    "name" : "i64",
+    "type" : "long"
+  }, {
+    "name" : "f32",
+    "type" : "float"
+  }, {
+    "name" : "f64",
+    "type" : "double"
+  }, {
+    "name" : "s",
+    "type" : [ "null", "string" ],
+    "default" : null
+  }, {
+    "name" : "d",
+    "type" : {
+      "type" : "int",
+      "logicalType" : "date"
+    }
+  }, {
+    "name" : "t",
+    "type" : {
+      "type" : "int",
+      "logicalType" : "time-millis"
+    }
+  }, {
+    "name" : "ts",
+    "type" : {
+      "type" : "long",
+      "logicalType" : "timestamp-millis"
+    }
+  } ]
+}
+
diff --git a/lang/java/compiler/pom.xml b/lang/java/compiler/pom.xml
index 42e6d27..678ccf8 100644
--- a/lang/java/compiler/pom.xml
+++ b/lang/java/compiler/pom.xml
@@ -129,6 +129,10 @@
       <!-- can only be used from within ant -->
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>joda-time</groupId>
+      <artifactId>joda-time</artifactId>
+    </dependency>
   </dependencies>
 
 </project>
diff --git a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
index 823a2ef..6bf7bd5 100644
--- a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
+++ b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
@@ -34,6 +34,11 @@
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.avro.Conversion;
+import org.apache.avro.LogicalTypes;
+import org.apache.avro.data.TimeConversions.DateConversion;
+import org.apache.avro.data.TimeConversions.TimeConversion;
+import org.apache.avro.data.TimeConversions.TimestampConversion;
 import org.apache.avro.specific.SpecificData;
 import org.codehaus.jackson.JsonNode;
 
@@ -86,6 +91,13 @@
     PUBLIC, PUBLIC_DEPRECATED, PRIVATE
   }
 
+  private static final SpecificData SPECIFIC = new SpecificData();
+  static {
+    SPECIFIC.addLogicalTypeConversion(new DateConversion());
+    SPECIFIC.addLogicalTypeConversion(new TimeConversion());
+    SPECIFIC.addLogicalTypeConversion(new TimestampConversion());
+  }
+
   private final Set<Schema> queue = new HashSet<Schema>();
   private Protocol protocol;
   private VelocityEngine velocityEngine;
@@ -552,6 +564,12 @@
 
   /** Utility for template use.  Returns the java type for a Schema. */
   public String javaType(Schema schema) {
+    Conversion<?> conversion = SPECIFIC
+        .getConversionFor(schema.getLogicalType());
+    if (conversion != null) {
+      return conversion.getConvertedType().getName();
+    }
+
     switch (schema.getType()) {
     case RECORD:
     case ENUM:
@@ -583,6 +601,12 @@
 
   /** Utility for template use.  Returns the unboxed java type for a Schema. */
   public String javaUnbox(Schema schema) {
+    Conversion<?> conversion = SPECIFIC
+        .getConversionFor(schema.getLogicalType());
+    if (conversion != null) {
+      return conversion.getConvertedType().getName();
+    }
+
     switch (schema.getType()) {
     case INT:     return "int";
     case LONG:    return "long";
@@ -593,6 +617,26 @@
     }
   }
 
+  public boolean hasLogicalTypeField(Schema schema) {
+    for (Schema.Field field : schema.getFields()) {
+      if (field.schema().getLogicalType() != null) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public String conversionInstance(Schema schema) {
+    if (LogicalTypes.date().equals(schema.getLogicalType())) {
+      return "DATE_CONVERSION";
+    } else if (LogicalTypes.timeMillis().equals(schema.getLogicalType())) {
+      return "TIME_CONVERSION";
+    } else if (LogicalTypes.timestampMillis().equals(schema.getLogicalType())) {
+      return "TIMESTAMP_CONVERSION";
+    }
+    return "null";
+  }
+
   /** Utility for template use.  Returns the java annotations for a schema. */
   public String[] javaAnnotations(JsonProperties props) {
     JsonNode value = props.getJsonProp("javaAnnotation");
diff --git a/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj b/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
index 8f60b83..775a8cb 100644
--- a/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
+++ b/lang/java/compiler/src/main/javacc/org/apache/avro/compiler/idl/idl.jj
@@ -69,6 +69,7 @@
 import java.net.URL;
 
 import org.apache.avro.Schema;
+import org.apache.avro.LogicalTypes;
 import org.apache.avro.Schema.*;
 import org.apache.avro.Protocol;
 import org.apache.avro.Protocol.*;
@@ -236,6 +237,9 @@
 | < TRUE: "true" >
 | < UNION: "union" >
 | < VOID: "void" >
+| < DATE: "date" >
+| < TIME: "time_ms" >
+| < TIMESTAMP: "timestamp_ms" >
 }
 
 /* LITERALS */
@@ -1481,6 +1485,9 @@
 | "double" { return Schema.create(Type.DOUBLE); }
 | "long" { return Schema.create(Type.LONG); }
 | "null" { return Schema.create(Type.NULL); }
+| "date" { return LogicalTypes.date().addToSchema(Schema.create(Type.INT)); }
+| "time_ms" { return LogicalTypes.timeMillis().addToSchema(Schema.create(Type.INT)); }
+| "timestamp_ms" { return LogicalTypes.timestampMillis().addToSchema(Schema.create(Type.LONG)); }
 }
 
 /**
@@ -1545,6 +1552,9 @@
    t = <TRUE> |
    t = <UNION> |
    t = <VOID> |
+   t = <DATE> |
+   t = <TIME> |
+   t = <TIMESTAMP> |
    t = <IDENTIFIER>)
   {
     return t;
diff --git a/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm b/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm
index 3e26df5..3bbe1e6 100644
--- a/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm
+++ b/lang/java/compiler/src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/record.vm
@@ -107,6 +107,26 @@
     default: throw new org.apache.avro.AvroRuntimeException("Bad index");
     }
   }
+
+#if ($this.hasLogicalTypeField($schema))
+  protected static final org.apache.avro.data.TimeConversions.DateConversion DATE_CONVERSION = new org.apache.avro.data.TimeConversions.DateConversion();
+  protected static final org.apache.avro.data.TimeConversions.TimeConversion TIME_CONVERSION = new org.apache.avro.data.TimeConversions.TimeConversion();
+  protected static final org.apache.avro.data.TimeConversions.TimestampConversion TIMESTAMP_CONVERSION = new org.apache.avro.data.TimeConversions.TimestampConversion();
+
+  private final org.apache.avro.Conversion<?>[] conversions =
+      new org.apache.avro.Conversion<?>[] {
+#foreach ($field in $schema.getFields())
+      ${this.conversionInstance($field.schema())},
+#end
+      null
+  };
+
+  @Override
+  public org.apache.avro.Conversion<?> getConversion(int field) {
+    return conversions[field];
+  }
+
+#end
   // Used by DatumReader.  Applications should not call.
   @SuppressWarnings(value="unchecked")
   public void put(int field$, java.lang.Object value$) {
diff --git a/lang/java/compiler/src/test/idl/input/mr_events.avdl b/lang/java/compiler/src/test/idl/input/mr_events.avdl
index 63568fb..272891a 100644
--- a/lang/java/compiler/src/test/idl/input/mr_events.avdl
+++ b/lang/java/compiler/src/test/idl/input/mr_events.avdl
@@ -42,7 +42,7 @@
 
   record JobFinished {
     string jobid;
-    long finishTime;
+    timestamp_ms finishTime;
     int finishedMaps;
     int finishedReduces;
     int failedMaps;
@@ -54,7 +54,7 @@
 
   record JobInited {
     string jobid;
-    long launchTime;
+    timestamp_ms launchTime;
     int totalMaps;
     int totalReduces;
     string jobStatus;
@@ -64,7 +64,7 @@
     string jobid;
     string jobName;
     string userName;
-    long submitTime;
+    timestamp_ms submitTime;
     string jobConfPath;
   }
 
diff --git a/lang/java/compiler/src/test/idl/input/simple.avdl b/lang/java/compiler/src/test/idl/input/simple.avdl
index db22309..37aed9e 100644
--- a/lang/java/compiler/src/test/idl/input/simple.avdl
+++ b/lang/java/compiler/src/test/idl/input/simple.avdl
@@ -47,6 +47,8 @@
 
     double value = NaN;
     float average = -Infinity;
+    date d = 0;
+    time_ms t = 0;
   }
 
   error TestError {
diff --git a/lang/java/compiler/src/test/idl/output/mr_events.avpr b/lang/java/compiler/src/test/idl/output/mr_events.avpr
index 25da3a8..c4e694f 100644
--- a/lang/java/compiler/src/test/idl/output/mr_events.avpr
+++ b/lang/java/compiler/src/test/idl/output/mr_events.avpr
@@ -52,7 +52,7 @@
       "type" : "string"
     }, {
       "name" : "finishTime",
-      "type" : "long"
+      "type" : {"type": "long", "logicalType": "timestamp-millis"}
     }, {
       "name" : "finishedMaps",
       "type" : "int"
@@ -83,7 +83,7 @@
       "type" : "string"
     }, {
       "name" : "launchTime",
-      "type" : "long"
+      "type" : {"type": "long", "logicalType": "timestamp-millis"}
     }, {
       "name" : "totalMaps",
       "type" : "int"
@@ -108,7 +108,7 @@
       "type" : "string"
     }, {
       "name" : "submitTime",
-      "type" : "long"
+      "type" : {"type": "long", "logicalType": "timestamp-millis"}
     }, {
       "name" : "jobConfPath",
       "type" : "string"
@@ -116,4 +116,4 @@
   } ],
   "messages" : {
   }
-}
\ No newline at end of file
+}
diff --git a/lang/java/compiler/src/test/idl/output/simple.avpr b/lang/java/compiler/src/test/idl/output/simple.avpr
index 40cb58a..c3e18e2 100644
--- a/lang/java/compiler/src/test/idl/output/simple.avpr
+++ b/lang/java/compiler/src/test/idl/output/simple.avpr
@@ -46,6 +46,14 @@
       "name" : "average",
       "type" : "float",
       "default" : "-Infinity"
+    }, {
+      "name": "d",
+      "type": {"type": "int", "logicalType": "date"},
+      "default": 0
+    }, {
+      "name": "t",
+      "type": {"type": "int", "logicalType": "time-millis"},
+      "default": 0
     } ],
     "my-property" : {
       "key" : 3
@@ -109,4 +117,4 @@
       "one-way" : true
     }
   }
-}
\ No newline at end of file
+}
diff --git a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
index 7605724..3668d2f 100644
--- a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
+++ b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
@@ -35,11 +35,14 @@
 import java.util.Collection;
 import java.util.List;
 
+import com.google.common.io.Resources;
 import org.apache.avro.AvroTestUtil;
+import org.apache.avro.LogicalTypes;
 import org.apache.avro.Schema;
 import org.apache.avro.SchemaBuilder;
 import org.apache.avro.generic.GenericData.StringType;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -295,4 +298,35 @@
     assertThat("Generated files should contain the same characters in the proper encodings",
       new String(fileInDefaultEncoding), equalTo(new String(fileInDifferentEncoding, differentEncoding)));
   }
+
+  @Test
+  public void testLogicalTypes() throws Exception {
+    SpecificCompiler compiler = createCompiler();
+
+    Schema dateSchema = LogicalTypes.date()
+        .addToSchema(Schema.create(Schema.Type.INT));
+    Schema timeSchema = LogicalTypes.timeMillis()
+        .addToSchema(Schema.create(Schema.Type.INT));
+    Schema timestampSchema = LogicalTypes.timestampMillis()
+        .addToSchema(Schema.create(Schema.Type.LONG));
+    Schema decimalSchema = LogicalTypes.decimal(9,2)
+        .addToSchema(Schema.create(Schema.Type.BYTES));
+
+    Assert.assertEquals("Should use Joda LocalDate for date type",
+        "org.joda.time.LocalDate", compiler.javaType(dateSchema));
+    Assert.assertEquals("Should use Joda LocalTime for time-millis type",
+        "org.joda.time.LocalTime", compiler.javaType(timeSchema));
+    Assert.assertEquals("Should use Joda DateTime for timestamp-millis type",
+        "org.joda.time.DateTime", compiler.javaType(timestampSchema));
+    Assert.assertEquals("Should use underlying type when missing conversion",
+        "java.nio.ByteBuffer", compiler.javaType(decimalSchema));
+  }
+
+  @Test
+  public void testLogicalTypesWithMultipleFields() throws Exception {
+    Schema logicalTypesWithMultipleFields = new Schema.Parser().parse(
+        new File("src/test/resources/simple_record.avsc"));
+    assertCompilesWithJavaCompiler(
+        new SpecificCompiler(logicalTypesWithMultipleFields).compile());
+  }
 }
diff --git a/lang/java/compiler/src/test/resources/logical_types_with_multiple_fields.avsc b/lang/java/compiler/src/test/resources/logical_types_with_multiple_fields.avsc
new file mode 100644
index 0000000..7a05111
--- /dev/null
+++ b/lang/java/compiler/src/test/resources/logical_types_with_multiple_fields.avsc
@@ -0,0 +1,30 @@
+{
+  "namespace": "schema.common",
+  "type": "record",
+  "name": "Action",
+  "fields": [
+    {
+      "name": "name",
+      "type": "string"
+    },
+    {
+      "name": "uuid",
+      "type": "string"
+    },
+    {
+      "name": "time",
+      "type": {
+        "type": "long",
+        "logicalType": "timestamp-millis"
+      }
+    },
+    {
+      "name": "requestId",
+      "type": [
+        "null",
+        "string"
+      ],
+      "default": null
+    }
+  ]
+}
diff --git a/lang/java/pom.xml b/lang/java/pom.xml
index 4e5ddd8..93cab06 100644
--- a/lang/java/pom.xml
+++ b/lang/java/pom.xml
@@ -473,7 +473,6 @@
         <groupId>joda-time</groupId>
         <artifactId>joda-time</artifactId>
         <version>${joda.version}</version>
-        <optional>true</optional>
       </dependency>
     </dependencies>
   </dependencyManagement>