Discussions

News: Transparently adding metadata to POJOs

  1. Transparently adding metadata to POJOs (9 messages)

    RIFE 1.4 has implemented a mechanism that is able to add metadata to POJOs via the classloader without modifying the POJOs themselves. This feature is automatic, transparent as long as your POJO is on the classpath.

    Adding constraints is as easy as creating a class in the same package as the POJO, naming it with a MetaData suffix and using provided extension points. Using this standardized naming convention makes its purpose clear and also allows the classloader to find and associate your metadata class with your POJO.

    The magic happens when the classloader finds both. It uses bytecode engineering to dynamically merge the metadata class into the POJO, and implements all interfaces of the medata class through delegation.

    The simplest example of such metadata would look something like the following,

      public class Pet {
          private String name;

          public void setName(String name) { this.name = name; }
          public String getName() { return this.name; }
      }

      public class PetMetaData extends MetaData {
          public void activateMetaData() {
              addConstraint(new ConstrainedProperty("name")
                  .maxLength(10)
                  .notNull(true));
          }
      }

    Putting this all together (and under the control of the classloader) allows you to access the Pet class' other aspects simply by casting it to the other interfaces. Here you're casting to Validated, an interface providing a way to validate against some given constraints (and provided through MetaData),

      Pet pet = new Pet();
      Validated validated = (Validated)pet;
      assertFalse(validated.validate());
     
      validated.resetValidation();
     
      person.setName("Dogbert");
      assertTrue(validated.validate());

    Of course, the classloader needs to be enabled for all the magic to work. In a standalone environment this is as simple as,

      java com.uwyn.rife.test.RunWithEngineClassLoader YourMainClass

    In enterprise environments, say within JBoss or Tomcat, the RIFE classloader is enabled by default and the preferred manner to make use of it is as a simple filter from web.xml,

      <filter>
        <filter-name>RIFE</filter-name>
        <filter-class>com.uwyn.rife.servlet.RifeFilter</filter-class>
        <init-param>
          <param-name>rep.path</param-name>
          <param-value>rep/participants.xml</param-value>
        </init-param>
      </filter>
      <filter-mapping>
        <filter-name>RIFE</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>

    Like other tools using bytecode engineering (Hibernate, Spring, etc) RIFE's approach come at a price. Sun's security manager doesn't permit such use by default and it's not surprising that under many application servers (whether enabled by default or not) ends up being disabled.

    As this is the first release of this metadata approach, the RIFE project plans on further exploring its capabilities; such as the possibility of providing dynamic Active Record capabilities in plain Java based on existing database table structures.

    What do you think of this direction? Is it worthwhile stretching the capabilities of Java in such a fashion?
  2. The classloader/bytecode manipulation is cool, but couldn't you just do:

    Validated validated = Validator.getValidated(pet);
  3. Hi Dave,
    hmm it seems that the example has been chosen by Steph with RIFE's validation system in mind. It is a bit different than others in that it collects validation errors and invalid subjects in a Validated object instance. It's very convenient to be able to keep this together with the model that's actually being validated. Traditionally we did this by either extending a base class or implementing interfaces and delegate. With this new meta data approach however both can cleanly be dissociated while still having stateful delegation by associating an instance of the meta data class with one particular instance of the model.

    Hope this clarifies this a bit.

    Best regards,

    Geert
  4. I don't pretend to fully understand what your MetaData supertype does, but in AspectJ if you wanted to make Pet support Validation as a separate aspect, you'd write this:

    public aspect PetValidationSupport {

      declare parents : Pet implements Validated;

      public boolean Pet.validate() {
         // validation implementation...
      }

      // other methods of Validated defined for Pet, or inherited via
      // AspectJ-defined default implementation

    }

    if this aspect is on the classpath, and you're using AspectJ's load-time weaving, you're done. (AspectJ supports Java 5 javaagent, JRockit agent, classloader-based weaving etc..). It comes with good tool support, a battle-hardened implementation, ability to do more than just meta-data etc..

    So modular implementation of validation interfaces etc. certainly has some merit - but you could get there a lot faster by piggy-backing on AspectJ rather than rolling your own bytecode transformation tool....

    (and you can always use the @AspectJ annotations if you don't like the aspectj keywords)
  5. The MetaData super type is not needed, the only thing that actually needs to be present is the MetaDataMerged interface. This will make the base class automatically implement the interfaces of the meta data class through delegation. I have not much experience with AspectJ, but afaik, what you describe adds method implementations, but doesn't delegate to a dedicated meta data instance. This requires additional handling and byte-code rewriting when cloning is performed for instance, as both the base instance and the meta data instance need to be correctly associated in the clones.

    Also, our approach doesn't need any tool support or special keywords. It's all pure Java and the only 'magic' for the user is the MetaData suffix on the class name. This makes it very intuitive and easy to use and comprehend.
  6. The simplest example of such metadata would look something like the following,

      public class Pet {
          private String name;

          public void setName(String name) { this.name = name; }
          public String getName() { return this.name; }
      }

      public class PetMetaData extends MetaData {
          public void activateMetaData() {
              addConstraint(new ConstrainedProperty("name")
                  .maxLength(10)
                  .notNull(true));
          }
      }

    I don't agree. The simplest way to work with Java is to keep field name strings out of code, so it stays refactorable and typesafe. For constraints you would also want to use method calls on fields instead of reinventing a parallel constraint language with methods like #maxLength() or #notNull(). How about this:

    public class PetMetaData extends MetaData {
      public boolean activateConstraints(Pet pet){
        return pet.getName() != null
            && pet.getName().length <= 10;
      }
    }

    The above can also be bytecode-engineered perfectly to be integrated with the original class. We do something similar with Native Queries:
    http://www.db4o.com/about/productinformation/whitepapers/#nq
  7. Carl, you can write those if you want. By implementing MetaDataBeanAware, you received the instance of person that it's associated with.

    However, you are doing there the real validation, you don't provide meta data that can be interpreted by any kind of layer. MaxLength and NotNull can both be used by database structure creation logic, form building logic, validation logic, ...
  8. MaxLength and NotNull can both be used by database structure creation logic, form building logic, validation logic, ...

    Why don't you transform { String#length() } and { Object != null } to database structure creation logic, form building logic and validation logic?

    That's exactly what we are doing with Native Queries: We allow users to write plain Java and analyze Java bytecode to be able to execute indexed queries without running the actual code the user writes.
  9. Since these are merely the most simplest of constraints. Adding constraints about the mime type, content transformation, dimension attributes, etc etc are not directly mappable to the Java type of the bean property. For example:

    addConstraint(new ConstrainedProperty("imageMedium")
        .mimeType(MimeType.IMAGE_JPEG)
        .contentAttribute("width", 120)
        .editable(false));

    addConstraint(new ConstrainedProperty("text")
        .mimeType(MimeType.APPLICATION_XHTML)
        .autoRetrieved(true)
        .fragment(true)
        .transformer(transformer));
  10. Why?[ Go to top ]

    Could you please spare a thought for the people who will have to work on this later. Transparent means invisible.

    If this breaks or needs change, you're in trouble. As soon as you have to change it, you are faced with a load of invisible layers of sticky stuff, which will eventually suffocate you. The reaction of the average programmer is to just put a hack somewhere which will seem to work at first glance.

    Things like EJBs, ORM tools, etc hide a lot of details from you, but at least they are standard enough for people to dig into the details and familiarise themselves with the oddities. In the case of Hibernate (example given by the poster) it is invisible and will stay so, unless it breaks, and even then the cause of the breakage might be visible. This is not.

    In the example given, to achieve validation of a property, on top of writing a new class, one has to consider these - classpaths (in IDE and build files and runtime), classloading mechanism (potentially different between environments and servers), security policy (let's not even start on this), packaging, developer training, maintenance.

    Now, compare to this:

    void setName(String name) throws ValidationException {
      if (!validator.validatePersonsName(name)) {
        throw new ValidationFailedException(...);
      }
      this.name = name;
    }

    Yes, I know, it's not meant for simple cases like this.... But it's open to abuse all the same, isn't it? And people will abuse it just to prove how clever they are, just like with everything else so far. And how messy can it get in a real complex case? In a real project learning little clevernesses like this _properly_ and applying them _properly_ is well near the bottom of priorities. Most people will be just happy to see it work after days of debugging somebody else's code.

    In short, this is not a pragmatic direction to take in my opinion....