Informatics 102 Spring 2012
Assignment #2: Metadata and Reflection

Due date and time: Wednesday, May 2, 11:59pm


Introduction

Most programming languages provide a set of keywords, which allow you to communicate elements of your program's meaning to a language processor (a compiler or interpreter). In most cases, these keywords are reserved, too, meaning that they are not permitted to be used as identifiers (e.g., the names of methods, classes, etc.); they can only be used to mean what the language defines them to mean. This usually small set of keywords forms part of the backbone of the syntax and semantics of the language — it's part of what makes the language distinctive and recognizable — so it's reasonable for their meanings not to be configurable.

Java, of course, has its own set of keywords. Classes are identified by the keyword class; loops are identified by the keywords for, while, and do; immutability of various kinds is specified by the keyword final; access is controlled by public, private, and protected; and so on. The set of keywords available in Java is limited and changes only rarely. In fact, powerful forces counteract this kind of change. One is the need for compatibility of older Java programs with newer versions of the language, which can become a problem if a keyword is adopted that matches identifiers used in older programs. (This is why, when the "foreach" loop was added to the Java language, it was decided to denote it by the keyword for, rather than something more specific like foreach.) Another is the desire to prevent languages from becoming so bloated with special-purpose keywords that they become prohibitively difficult to understand, to learn, to teach, and even to compile.

Being what we call a general-purpose programming language, Java attempts to be useful in a wide variety of problem domains, while not being strongly biased toward any one of them. We don't only use Java to write line-of-business applications that run on desktops with graphical user interfaces, or to write back-end business logic for web applications, or to write "middleware" that connects distributed applications together, or to write games that run on mobile devices, or to write automated tests. We can use Java to write any of those things. That these are different problem domains implies that they have different requirements, as well as different design elements that we'd need to encode. But one language, with one set of keywords, places a limit on what we can specify succinctly. When we try to bend Java too far in the direction of being specific to one domain, we sometimes have to do a fair amount of work to make it do what we want, even though Java is capable of doing it. We should want better; all else being equal, a succinct program is better than a longer, more complicated one.

One solution to this problem, which we'll explore later this quarter, is to design your own small languages that are targeted at issues specific to your problem domain. then use these languages in areas of the application where they apply. But even within the confines of a general-purpose language like Java, we can write dramatically simpler, more flexible designs when we can include metadata in our programs. Metadata are not themselves general-purpose program instructions; instead, they are special markings, which we define and whose meanings we choose, that communicate important, domain-specific concepts about constructs in our programs. So long as those markings are available to language processing tools like compilers (or, say, preprocessors that run before the compiler does and know something about our problem domain) or are available and accessible at run-time, they can be used to affect the meaning of our programs, allowing us, effectively, to introduce new keywords that are specific to a particular program or kind of program, but which do not require changes to the underlying programming language in order to support them.

Java provides the ability to place metadata into our programs in the form of annotations. While there are some built-in annotations in the Java library, like @SuppressWarnings and @WebService, we also have the option of creating our own, specific to the problem that we're trying to solve. Annotations can be placed on many Java constructs, including classes, methods, constructors, parameters, fields, and local variables. Some annotations are available only at compile-time, while others are available to the program at run-time.

Annotations available at run-time are only useful if there's some mechanism to allow your program to find them. Java provides one under the umbrella of a larger mechanism called reflection, which allows a program to find out about its own structure at run-time, letting the program determine what class an object belongs to, what methods that class supports, what annotations have been placed on those classes and methods, and so on. On the basis of this information, objects of the reflected classes can be created, methods can be called on objects, fields can be set, and so on. What makes this interesting is that it is all done dynamically, so that the same code can be used to create different kinds of objects, call different methods, set different fields, and so on, without those classes, objects, methods, or fields ever being explicitly named in the code.

Reflection may sound strange at first, but it has many practical uses. When your program can find out about its own structure at run-time, you can write code that will behave differently, automatically, depending on that structure. Annotations are important in this context because they provide something to search for, as well as a means of configuration. Consider, for example, how JUnit — a popular Java unit testing framework — determines which methods are tests: it looks for methods with the @Test annotation and calls them automatically, so that you never have to write — and, most importantly, you never have to maintain — code that explicitly calls them.

In this assignment, you'll explore the use of Java annotations and reflection in the implementation of a plug-in architecture, where a program allows annotated code comforming to a set of rules to be plugged into it without being aware of that code ahead of time, in the same way that Eclipse is able to execute plug-ins like the AJDT plug-in for AspectJ. You'll also learn how to use AspectJ to turn certain run-time errors into compile-time errors, a useful technique to assist plug-in writers in meeting the requirements of a plug-in architecture.


The problem

In this assignment, you are given a simple image viewing application written in Java that supports the ability to make image modifications using "filters." Built into the application is a single filter, which horizontally mirrors an image. You will add two things to the application:

  1. Plug-in support, allowing the application to load filter plug-ins identified by a configuration file. Plug-in filters are to be marked with annotations and loaded dynamically using reflection.
  2. An aspect, written in AspectJ, that can be used to enforce compile-time checking on plug-ins, turning violations of the plug-in architecture's rules into compile-time errors.

Starting point

The provided code

A complete version of the application, minus plug-in support and the aspect you're required to write, is being provided in a zip archive, which you'll want to extract into the src folder of a new AspectJ Project in Eclipse. The provided code is arranged into the following packages:

How the provided application works

The provided version of the application has a simple user interface that allows you to open and close image files in any format supported natively by Java (at least JPEG, PNG, GIF, and BMP). Only one image can be open at a time; it is necessary to close an open image before opening another. (Note, too, that particularly large image files — more than a couple of megabytes — may not load without the program becoming very slow or even running out of memory; this is mostly because of the way I load the files, so that they will be easily accessible to you as a two-dimensional array of objects, which can require a lot of memory for a large image. A more practical image editor would take a very different approach.)

The Filter menu contains a list of all filters supported by the application. In the provided version, there will be only one filter, called Mirror Horizontally. Once your plug-in support is added, the application will be able to load a collection of additional filters, though Mirror Horizontally will always appear on the list.

Familiarizing yourself with the application and its code

Before proceeding further, it would be wise to set up an Eclipse project containing all of the code for the application, familiarize yourself with the application by running it and experimenting with it, then read through the comments in the provided code.


Part 1: Adding plug-in support to the application

A description of the plug-in architecture

The plug-in architecture for our image viewer requires each plug-in to be written in a method that conforms to the following requirements:

Methods that do not conform to these requirements do not represent plug-ins, so the application should quietly ignore them.

The format of the configuration file

The configuration file is a simple list of fully-qualified class names, one per line, that contain plug-in filter methods that conform to the requirements below. By "fully-qualified," I mean that the class names include the name of the package that they're written in. An example configuration file for a hypothetical set of plug-ins is below:

inf102.assignment2.plugins.NewPlugIns
hello.there.MorePlugIns

What you'll need to do

The inf102.assignment2.plugins.FilterPlugInLoader class contains a method called loadFilters(). This is the starting point for the code you'll need to write to load plug-ins. You'll need to read the names of the classes listed in the configuration file, then load the plug-ins from each of these classes using reflection. Your method will need to return objects that implement the Filter interface and make the appropriate calls to the reflected methods.

Be sure to do run-time checking and skip filters that do not conform to the requirements listed above.


Part 2: Using AspectJ to check plug-in requirements at compile time

Plug-in methods are required to meet certain requirements, which are listed in the previous part. For this part, write one or more aspects in AspectJ that verify that methods with the @FilterPlugIn annotation meet these requirements. For any of these requirements that is not met, raise a compile-time error. You do not need to have a separate error message for each kind of violation, though you can if you'd like.

It may not be possible to check all of the requirements above using AspectJ. For any requirement that cannot be checked, briefly describe in a comment in one of your aspects why it couldn't be.


Deliverables

Submit all of the code in the inf102.assignment2.plugins package, including your aspect(s) (which should be written in the same package) and any code that we provided, to Checkmate. You do not need to submit any plug-ins, though you're welcome to submit any that you think are interesting. Do not submit any of the .class files or files generated by your development environment.

Java classes should be written in files whose names end in .java. Aspects should be written in files whose names end in .aj.

Follow this link for a discussion of how to submit your assignment via Checkmate. Be aware that I'll be holding you to all of the rules specified in that document, including the one that says that you're responsible for submitting the version of the assignment that you want graded. We won't regrade an assignment simply because you submitted the wrong version by accident.