O'Reilly logo

Java Data Objects by Craig Russell, David Jordan

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Operations on Instances

Now we have a datastore in which we can store instances of our classes. Each application needs to acquire a PersistenceManager to access and update the datastore. Example 1-8 provides the source for the MediaManiaApp class, which serves as the base class for each application in this book. Each application is a concrete subclass of MediaManiaApp that implements its application logic in the execute( ) method.

MediaManiaApp has a constructor that loads the properties from jdo.properties (line [1]). After loading properties from the file, it calls getPropertyOverrides( ) and merges the returned properties into jdoproperties. An application subclass can redefine getPropertyOverrides( ) to provide any additional properties or change properties that are set in the jdo.properties file. The constructor gets a PersistenceManagerFactory (line [2]) and then acquires a PersistenceManager (line [3]). We also provide the getPersistenceManager( ) method to access the PersistenceManager from outside the MediaManiaApp class. The Transaction associated with the PersistenceManager is acquired on line [4].

The application subclasses make a call to executeTransaction( ), defined in the MediaManiaApp class. This method begins a transaction on line [5]. It then calls execute( ) on line [6], which will execute the subclass-specific functionality.

We chose this particular design for application classes to simplify and reduce the amount of redundant code in the examples for establishing an environment to run. This is not required in JDO; you can choose an approach that is best suited for your application environment.

After the return from the execute( ) method (implemented by a subclass), an attempt is made to commit the transaction (line [7]). If any exceptions are thrown, the transaction is rolled back and the exception is printed to the error stream.

Example 1-8. MediaManiaApp base class

package com.mediamania;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.PersistenceManager;
import javax.jdo.Transaction;

public abstract class MediaManiaApp {
    protected PersistenceManagerFactory pmf;
    protected PersistenceManager        pm;
    protected Transaction               tx;
    
    public abstract void execute(  );  // defined in concrete application subclasses

    protected static Map getPropertyOverrides(  ) {
        return new HashMap(  );
    }
    public MediaManiaApp(  ) {
        try {
            InputStream propertyStream = new FileInputStream("jdo.properties");
            Properties jdoproperties = new Properties(  );
            jdoproperties.load(propertyStream);     [1]
            jdoproperties.putAll(getPropertyOverrides(  ));
            pmf = JDOHelper.getPersistenceManagerFactory(jdoproperties);     [2]
            pm = pmf.getPersistenceManager(  );     [3]
            tx = pm.currentTransaction(  );     [4]
        } catch (Exception e) {
            e.printStackTrace(System.err);
            System.exit(-1);
        }
    }
    public PersistenceManager getPersistenceManager(  ) {
        return pm;
    }
    public void executeTransaction(  ) {
        try {
            tx.begin(  );     [5]
            execute(  );     [6]
            tx.commit(  );     [7]
        } catch (Throwable exception) {
            exception.printStackTrace(System.err);
            if (tx.isActive()) tx.rollback(  );
        }
    }
}

Making Instances Persistent

Let’s examine a simple application, called CreateMovie, that makes a single Movie instance persistent, as shown in Example 1-9. The functionality of the application is placed in execute( ). After constructing an instance of CreateMovie, we call executeTransaction( ), which is defined in the MediaManiaApp base class. It makes a call to execute( ), which will be the method defined in this class. The execute( ) method instantiates a single Movie instance on line [5]. Calling the PersistenceManager method makePersistent( ) on line [6] makes the Movie instance persistent. If the transaction commits successfully in executeTransaction( ), the Movie instance will be stored in the datastore.

Example 1-9. Creating a Movie instance and making it persistent

package com.mediamania.prototype;

import java.util.Calendar;
import java.util.Date;
import com.mediamania.MediaManiaApp;

public class CreateMovie extends MediaManiaApp {
    public static void main(String[] args) {
        CreateMovie createMovie = new CreateMovie(  );
        createMovie.executeTransaction(  );
    }
    public void execute(  ) {
        Calendar cal = Calendar.getInstance(  );
        cal.clear(  );
        cal.set(Calendar.YEAR, 1997);
        Date date = cal.getTime(  );
        Movie movie = new Movie("Titanic", date, 194, "PG-13", "historical, drama");     [5]
        pm.makePersistent(movie);     [6]
    }
}

Now let’s examine a larger application. LoadMovies, shown in Example 1-10, reads a file containing movie data and creates multiple instances of Movie. The name of the file is passed to the application as an argument, and the LoadMovies constructor initializes a BufferedReader to read the data. The execute( ) method reads one line at a time from the file and calls parseMovieData( ), which parses the line of input data, creates a Movie instance on line [1], and makes it persistent on line [2]. When the transaction commits in executeTransaction( ), all of the newly created Movie instances will be stored in the datastore.

Example 1-10. LoadMovies

package com.mediamania.prototype;

import java.io.FileReader;
import java.io.BufferedReader;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import javax.jdo.PersistenceManager;
import com.mediamania.MediaManiaApp;

public class LoadMovies extends MediaManiaApp {
    private BufferedReader  reader;

    public static void main(String[] args) {
        LoadMovies loadMovies = new LoadMovies(args[0]);
        loadMovies.executeTransaction(  );
    }
    public LoadMovies(String filename) {
        try {
            FileReader fr = new FileReader(filename);
            reader = new BufferedReader(fr);
        } catch (Exception e) {
            System.err.print("Unable to open input file ");
            System.err.println(filename);
            e.printStackTrace(  );
            System.exit(-1);
        }
    }
    public void execute(  ) {
        try {
            while ( reader.ready(  ) ) {
                String line = reader.readLine(  );
                parseMovieData(line);
            }
        } catch (java.io.IOException e) {
            System.err.println("Exception reading input file");
            e.printStackTrace(System.err);
        }
    }
    public void parseMovieData(String line) {
        StringTokenizer tokenizer = new StringTokenizer(line, ";");
        String title = tokenizer.nextToken(  );
        String dateStr = tokenizer.nextToken(  );
        Date releaseDate = Movie.parseReleaseDate(dateStr);
        int runningTime = 0;
        try {
            runningTime = Integer.parseInt(tokenizer.nextToken(  ));
        } catch (java.lang.NumberFormatException e) {
            System.err.print("Exception parsing running time for ");
            System.err.println(title);
        }
        String rating = tokenizer.nextToken(  );
        String genres = tokenizer.nextToken(  );
        Movie movie = new Movie(title, releaseDate, runningTime, rating, genres);     [1]
        pm.makePersistent(movie);     [2]
    }
}

The movie data is in a file with the following format:

movie title;release date;running time;movie rating;genre1,genre2,genre3

The format to use for release dates is maintained in the Movie class, so parseReleaseDate( ) is called to create a Date instance from the input data. A movie is described by one or more genres, which are listed at the end of the line of data.

Accessing Instances

Now let’s access the Movie instances in the datastore to verify that they were stored successfully. There are several ways to access instances in JDO:

  • Iterate an extent

  • Navigate the object model

  • Execute a query

An extent is a facility used to access all the instances of a particular class or the class and all its subclasses. If the application wants to access only a subset of the instances, a query can be executed with a filter that constrains the instances returned to those that satisfy a Boolean predicate. Once the application has accessed an instance from the datastore, it can navigate to related instances in the datastore by traversing through references and iterating collections in the object model. Instances that are not yet in memory are read from the datastore on demand. These facilities for accessing instances are often used in combination, and JDO ensures that each persistent instance is represented in the application memory only once per PersistenceManager. Each PersistenceManager manages a single transaction context.

Iterating an extent

JDO provides the Extent interface for accessing the extent of a class. The extent allows access to all of the instances of a class, but using an extent does not imply that all the instances are in memory. The PrintMovies application, provided in Example 1-11, uses the Movie extent.

Example 1-11. Iterating the Movie extent

package com.mediamania.prototype;

import java.util.Iterator;
import java.util.Set;
import javax.jdo.PersistenceManager;
import javax.jdo.Extent;
import com.mediamania.MediaManiaApp;

public class PrintMovies extends MediaManiaApp {

    public static void main(String[] args) {
        PrintMovies movies = new PrintMovies(  );
        movies.executeTransaction(  );
    }

    public void execute(  ) {
        Extent extent = pm.getExtent(Movie.class, true);     [1]
        Iterator iter = extent.iterator(  );     [2]
        while (iter.hasNext(  )) {
            Movie movie = (Movie) iter.next(  );     [3]
            System.out.print(movie.getTitle(  ));             System.out.print(";");
            System.out.print(movie.getRating(  ));            System.out.print(";");
            System.out.print(movie.formatReleaseDate(  ) );   System.out.print(";");
            System.out.print(movie.getRunningTime(  ));       System.out.print(";");
            System.out.println(movie.getGenres(  ));     [4]
        
            Set cast = movie.getCast(  );     [5]
            Iterator castIterator = cast.iterator(  );
            while (castIterator.hasNext(  )) {
                Role role = (Role) castIterator.next(  );     [6]
                System.out.print("\t");
                System.out.print(role.getName(  ));
                System.out.print(", ");
                System.out.println(role.getActor().getName(  ));     [7]
            }
        }
        extent.close(iter);     [8]
    }
}

On line [1] we acquire an Extent for the Movie class from the PersistenceManager. The second parameter indicates whether to include instances of Movie subclasses. A value of false causes only Movie instances to be returned, even if there are instances of subclasses. Though we don’t currently have any classes that extend the Movie class, providing a value of true will return instances of any such classes that we may define in the future. The Extent interface has the iterator( ) method, which we call on line [2] to acquire an Iterator that will access each element of the extent. Line [3] uses the Iterator to access Movie instances. The application can then perform operations on the Movie instance to acquire data about the movie to print. For example, on line [4] we call getGenres( ) to get the genres associated with the movie. On line [5] we acquire the set of Roles. We acquire a reference to a Role on line [6] and then print the role’s name. On line [7] we navigate to the Actor for that role by calling getActor( ), which we defined in the Role class. We then print the actor’s name.

Once the application has completed iteration through the extent, line [8] closes the Iterator to relinquish any resources required to perform the extent iteration. Multiple Iterator instances can be used concurrently on an Extent. This method closes a specific Iterator; closeAll( ) closes all the Iterator instances associated with an Extent.

Navigating the object model

Example 1-11 demonstrates iteration of the Movie extent. But on line [6] we also navigate to a set of related Role instances by iterating a collection in our object model. On line [7] we use the Role instance to navigate through a reference to the related Actor instance. Line [5] and [7] demonstrate, respectively, traversal of to-many and to-one relationships. A relationship from one class to another has a cardinality that indicates whether there are one or multiple associated instances. A reference is used for a cardinality of one, and a collection is used when there can be more than one instance.

The syntax needed to access these related instances corresponds to the standard practice of navigating instances in memory. The application does not need to make any direct calls to JDO interfaces between lines [3] and [7]. It simply traverses among objects in memory. The related instances are not read from the datastore and instantiated in memory until they are accessed directly by the application. Access to the datastore is transparent; instances are brought into memory on demand. Some implementations provide facilities separate from the Java interface that allow you to influence the implementation’s access and caching algorithms. Your Java application is insulated from these optimizations, but it can take advantage of them to affect its overall performance.

The access of related persistent instances in a JDO environment is identical to the access of transient instances in a non-JDO environment, so you can write your software in a manner that is independent of its use in a JDO environment. Existing software written without any knowledge of JDO or any other persistence concerns is able to navigate objects in the datastore through JDO. This capability yields dramatic increases in development productivity and allows existing software to be incorporated into a JDO environment quickly and easily.

Executing a query

It is also possible to perform a query on an Extent. The JDO Query interface is used to select a subset of the instances that meet certain criteria. The remaining examples in this chapter need to access a specific Actor or Movie based on a unique name. These methods, shown in Example 1-12, are virtually identical; getActor( ) performs a query to get an Actor based on a name, and getMovie( ) performs a query to get a Movie based on a name.

Example 1-12. Query methods in the PrototypeQueries class

package com.mediamania.prototype;

import java.util.Collection;
import java.util.Iterator;
import javax.jdo.PersistenceManager;
import javax.jdo.Extent;
import javax.jdo.Query;

public class PrototypeQueries {
    public static Actor getActor(PersistenceManager pm, String actorName)
    {
        Extent actorExtent = pm.getExtent(Actor.class, true);     [1]
        Query query = pm.newQuery(actorExtent, "name == actorName");     [2]
        query.declareParameters("String actorName");     [3]
        Collection result = (Collection) query.execute(actorName);     [4]
        Iterator iter = result.iterator(  );
        Actor actor = null;
        if (iter.hasNext()) actor = (Actor)iter.next(  );     [5]
        query.close(result);     [6]
        return actor;
    }
    public static Movie getMovie(PersistenceManager pm, String movieTitle)
    {
        Extent movieExtent = pm.getExtent(Movie.class, true);
        Query query = pm.newQuery(movieExtent, "title == movieTitle");
        query.declareParameters("String movieTitle");
        Collection result = (Collection) query.execute(movieTitle);
        Iterator iter = result.iterator(  );
        Movie movie = null;
        if (iter.hasNext()) movie = (Movie)iter.next(  );
        query.close(result);
        return movie;
    }
}

Let’s examine getActor( ). On line [1] we get a reference to the Actor extent. Line [2] creates an instance of Query using the newQuery( ) method defined in the PersistenceManager interface. The query is initialized with the extent and a query filter to apply to the extent.

The name identifier in the filter is the name field in the Actor class. The namespace used to determine how to interpret the identifier is based on the class of the Extent used to initialize the Query instance. The filter expression requires that an Actor’s name field is equal to actorName. In the filter we can use the == operator directly to compare two Strings, instead of using the Java syntax (name.equals(actorName)).

The actorName identifier is a query parameter, which is declared on line [3]. A query parameter lets you provide a value to be used when the query is executed. We have chosen to use the same name, actorName, for the method parameter and query parameter. This practice is not required, and there is no direct association between the names of our Java method parameters and our query parameters. The query is executed on line [4], passing getActor( )’s actorName parameter as the value to use for the actorName query parameter.

The result type of Query.execute( ) is declared as Object. In JDO 1.0.1, the returned instance is always a Collection, so we cast the query result to a Collection. It is declared in JDO 1.0.1 to return Object, to allow for a future extension of returning a value other than a Collection. Our method then acquires an Iterator and, on line [5], attempts to access an element. We assume here that there can only be a single Actor instance with a given name. Before returning the result, line [6] closes the query result to relinquish any associated resources. If the method finds an Actor instance with the given name, the instance is returned. Otherwise, if the query result has no elements, a null is returned.

Modifying an Instance

Now let’s examine two applications that modify instances in the datastore. Once an application has accessed an instance from the datastore in a transaction, it can modify one or more fields of the instance. When the transaction commits, all modifications that have been made to instances are propagated to the datastore automatically.

The UpdateWebSite application provided in Example 1-13 is used to set the web site associated with a movie. It takes two arguments: the first is the movie’s title, and the second is the movie’s web site URL. After initializing the application instance, executeTransaction( ) is called, which calls the execute( ) method defined in this class.

Line [1] calls getMovie( ) (defined in Example 1-12) to retrieve the Movie with the given title. If getMovie( ) returns null, the application reports that it could not find a Movie with the given title and returns. Otherwise, on line [2] we call setWebSite( ) (defined for the Movie class in Example 1-1), which sets the webSite field of Movie to the parameter value. When executeTransaction( ) commits the transaction, the modification to the Movie instance is propagated to the datastore automatically.

Example 1-13. Modifying an attribute

package com.mediamania.prototype;

import com.mediamania.MediaManiaApp;

public class UpdateWebSite extends MediaManiaApp {
    private String  movieTitle;
    private String  newWebSite;

    public static void main (String[] args) {
        String title = args[0];
        String website = args[1];
        UpdateWebSite update = new UpdateWebSite(title, website);
        update.executeTransaction(  );
    }
    public UpdateWebSite(String title, String site) {
        movieTitle = title;
        newWebSite = site;
    }
    public void execute(  ) {
        Movie movie = PrototypeQueries.getMovie(pm, movieTitle);     [1]
        if (movie == null) {
            System.err.print("Could not access movie with title of ");
            System.err.println(movieTitle);
            return;
        }
        movie.setWebSite(newWebSite);     [2]
    }
}

As you can see in Example 1-13, the application does not need to make any direct JDO interface calls to modify the Movie field. This application accesses an instance and calls a method to modify the web site field. The method modifies the field using standard Java syntax. No additional programming is necessary prior to commit in order to propagate the data to the datastore. The JDO environment propagates the modifications automatically. This application performs an operation on persistent instances, yet it does not directly import or use any JDO interfaces.

Now let’s examine a larger application, called LoadRoles, that exhibits several JDO capabilities. LoadRoles, shown in Example 1-14, is responsible for loading information about the movie roles and the actors who play them. LoadRoles is passed a single argument that specifies the name of a file to read, and the constructor initializes a BufferedReader to read the file. It reads the text file, which contains one role per line, in the following format:

movie title;actor's name;role name

Usually, all the roles associated with a particular movie are grouped together in this file; LoadRoles performs a small optimization to determine whether the role information being processed is for the same movie as the previous role entry in the file.

Example 1-14. Instance modification and persistence-by-reachability

package com.mediamania.prototype;

import java.io.FileReader;
import java.io.BufferedReader;
import java.util.StringTokenizer;
import com.mediamania.MediaManiaApp;

public class LoadRoles extends MediaManiaApp {
    private BufferedReader  reader;

    public static void main(String[] args) {
        LoadRoles loadRoles = new LoadRoles(args[0]);
        loadRoles.executeTransaction(  );
    }
    public LoadRoles(String filename) {
        try {
            FileReader fr = new FileReader(filename);
            reader = new BufferedReader(fr);
        } catch(java.io.IOException e){
            System.err.print("Unable to open input file ");
            System.err.println(filename);
            System.exit(-1);
        }
    }
    public void execute(  ) {
        String lastTitle = "";
        Movie movie = null;
        try {
            while (reader.ready(  )) {
                String line = reader.readLine(  );
                StringTokenizer tokenizer = new StringTokenizer(line, ";");
                String title     = tokenizer.nextToken(  );
                String actorName = tokenizer.nextToken(  );
                String roleName  = tokenizer.nextToken(  );
                if (!title.equals(lastTitle)) {
                    movie = PrototypeQueries.getMovie(pm, title);     [1]
                    if (movie == null) {
                        System.err.print("Movie title not found: ");
                        System.err.println(title);
                        continue;
                    }
                    lastTitle = title;
                }
                Actor actor = PrototypeQueries.getActor(pm, actorName);     [2]
                if (actor == null) {
                    actor = new Actor(actorName);     [3]
                    pm.makePersistent(actor);     [4]
                }
                Role role = new Role(roleName, actor, movie);     [5]
            }
        } catch (java.io.IOException e) {
            System.err.println("Exception reading input file");
            System.err.println(e);
            return;
        }
    }
}

The execute( ) method reads each entry in the file. First, it checks to see whether the new entry’s movie title is the same as the previous entry. If it is not, line [1] calls getMovie( ) to access the Movie with the new title. If a Movie with that title does not exist in the datastore, the application prints an error message and skips over the entry. On line [2] we attempt to access an Actor instance with the specified name. If no Actor in the datastore has this name, a new Actor is created and given this name on line [3], and made persistent on line [4].

Up to this point in the application, we have just been reading the input file and looking up instances in the datastore that have been referenced by a name in the file. We perform the real task of the application on line [5], where we create a new Role instance. The Role constructor was defined in Example 1-3; it is repeated here so that we can examine it in more detail:

public Role(String name, Actor actor, Movie movie) {
    this.name = name;     [1]
    this.actor = actor;     [2]
    this.movie = movie;     [3]
    actor.addRole(this);     [4]
    movie.addRole(this);     [5]
}

Line [1] initializes the name of the Role. Line [2] establishes a reference to the associated Actor, and line [3] establishes a reference to the associated Movie instance. The relationships between Actor and Role and between Movie and Role are bidirectional, so it is also necessary to update the other side of each relationship. On line [4] we call addRole( ) on actor, which adds this Role to the roles collection in the Actor class. Similarly, line [5] calls addRole( ) on movie to add this Role to the cast collection field in the Movie class. Adding the Role as an element in Actor.roles and Movie.cast causes a modification to the instances referenced by actor and movie.

The Role constructor demonstrates that you can establish a relationship to an instance simply by initializing a reference to it, and you can establish a relationship with more than one instance by adding references to a collection. This process is how relationships are represented in Java and is supported directly by JDO. When the transaction commits, the relationships established in memory are preserved in the datastore.

Upon return from the Role constructor, execute( ) processes the next entry in the file. The while loop terminates once we have exhausted the contents of the file.

You may have noticed that we never called makePersistent( ) on the Role instances we created. Still, at commit, the Role instances are stored in the datastore because JDO supports persistence-by-reachability . Persistence-by-reachability causes any transient (nonpersistent) instance of a persistent class to become persistent at commit if it is reachable (directly or indirectly) by a persistent instance. Instances are reachable through either a reference or collection of references. The set of all instances reachable from a given instance is an object graph that is called the instance’s complete closure of related instances. The reachability algorithm is applied to all persistent instances transitively through all their references to instances in memory, causing the complete closure to become persistent.

Removing all references to a persistent instance does not automatically delete the instance. You need to delete instances explicitly, which we cover in the next section. If you establish a reference from a persistent instance to a transient instance during a transaction, but you change this reference and no persistent instances reference the transient instance at commit, it remains transient.

Persistence-by-reachability lets you write a lot of your software without having any explicit calls to JDO interfaces to store instances. Much of your software can focus on establishing relationships among the instances in memory, and the JDO implementation takes care of storing any new instances and relationships you establish among the instances in memory. Your applications can construct fairly complex object graphs in memory and make them persistent simply by establishing a reference to the graph from a persistent instance.

Deleting Instances

Now let’s examine an application that deletes some instances from the datastore. In Example 1-15, the DeleteMovie application is used to delete a Movie instance. The title of the movie to delete is provided as the argument to the program. Line [1] attempts to access the Movie instance. If no movie with the title exists, the application reports an error and returns. On line [6] we call deletePersistent( ) to delete the Movie instance itself.

Example 1-15. Deleting a Movie from the datastore

package com.mediamania.prototype;

import java.util.Collection;
import java.util.Set;
import java.util.Iterator;
import javax.jdo.PersistenceManager;
import com.mediamania.MediaManiaApp;

public class DeleteMovie extends MediaManiaApp {
    private String  movieTitle;

    public static void main(String[] args) {
        String title = args[0];
        DeleteMovie deleteMovie = new DeleteMovie(title);
        deleteMovie.executeTransaction(  );
    }
    public DeleteMovie(String title) {
        movieTitle = title;
    }
    public void execute(  ) {
        Movie movie = PrototypeQueries.getMovie(pm, movieTitle);     [1]
        if (movie == null) {
            System.err.print("Could not access movie with title of ");
            System.err.println(movieTitle);
            return;
        }
        Set cast = movie.getCast(  );     [2]
        Iterator iter = cast.iterator(  );
        while (iter.hasNext(  )) {
            Role role = (Role) iter.next(  );
            Actor actor = role.getActor(  );     [3]
            actor.removeRole(role);     [4]
        }
        pm.deletePersistentAll(cast);     [5]
        pm.deletePersistent(movie);     [6]
    }
}

But it is also necessary to delete the Role instances associated with the Movie. In addition, since an Actor includes a reference to the Role instance, it is necessary to remove this reference. On line [2] we access the set of Role instances associated with the Movie. We then iterate through each Role and access the associated Actor on line [3]. Since we will be deleting the Role instance, on line [4] we remove the actor’s reference to the Role. On line [5] we make a call to deletePersistentAll( ) to delete all the Role instances in the movie’s cast. When we commit the transaction, the Movie instance and associated Role instances are deleted from the datastore, and the Actor instances associated with the Movie are updated so that they no longer reference the deleted Role instances.

You must call these deletePersistent( ) methods explicitly to delete instances from the datastore. They are not the inverse of makePersistent( ), which uses the persistence-by-reachability algorithm. Furthermore, there is no JDO datastore equivalent to Java’s garbage collection, which deletes instances automatically once they are no longer referenced by any instances in the datastore. Implementing the equivalent of a persistent garbage collector is a very complex undertaking, and such systems often have poor performance.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required