Java 2

Week 6

 

Movie Project DAO

Course Objectives

  • Students will demonstrate an understanding multitier software design

  • Students will understand the use of standard collections and generics in the Java programming language

  • Students will understand elementary Thread programming in Java

  • Students will create software that performs standard data operations with Java and a relational database

  • Students will use streams to communicate with sockets and files

  • Students will apply elementary XML functionality in Java

  • Students will understand the importance of using standard Object Oriented Programming (OOP) design tools such as the Unified Modeling Language (UML)

  • Students will demonstrate a preference for messages and Exceptions for communicating between components

  • Students will systematize Test Driven Development

Data Sources

  • Create a new Java Maven project called Movies. Use the "edu.kirkwood" package.
  • I want to create a program that allows us to get Movie data from a variety of resources: XML, JSON, MySQL, and MongoDB.
  • To efficiently switch between different data sources, you should use the Data Access Object (DAO) pattern.
  • The core idea is to define a common interface that dictates what data operations can be performed (e.g., "get all movies"), and then create separate classes that implement this interface for each specific data source.
  • The main application will only interact with the interface, not the concrete classes, making it easy to swap the data source with minimal code changes.

Movie class

  • Create a "edu.kirkwood.model" package.
  • Create a class called "Movie". Add the following code.
package edu.kirkwood.model;

import java.util.Objects;

// A simple Plain Old Java Object (POJO)
public class Movie implements Comparable<Movie> {
    private String id;
    private String title;
    private int releaseYear;

    public Movie() {
    }

    public Movie(String id, String title, int releaseYear) {
        this.id = id;
        this.title = title;
        this.releaseYear = releaseYear;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getReleaseYear() {
        return releaseYear;
    }

    public void setReleaseYear(int releaseYear) {
        this.releaseYear = releaseYear;
    }

    @Override
    public String toString() {
        return "Movie{" +
                "id='" + id + '\'' +
                ", title='" + title + '\'' +
                ", releaseYear=" + releaseYear +
                '}';
    }

    /**
     * Compares two Movie objects by their release year (0-9), then title (A-Z)
     * @param o The other Movie being compared
     * @return an integer, in order if less than or equal to 0, not in order if greater than 0
     */
    @Override
    public int compareTo(Movie o) {
        int result = Integer.compare(this.releaseYear, o.getReleaseYear());
        if(result == 0) {
            result = this.title.compareTo(o.title);
        }
        return result;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Movie movie = (Movie) o;
        return releaseYear == movie.releaseYear && Objects.equals(title, movie.title);
    }

    @Override
    public int hashCode() {
        return Objects.hash(title, releaseYear);
    }
}

Movie XML data

<root totalResults="503" response="True">
  <result title="Batman Begins" year="2005" imdbID="tt0372784" type="movie" poster="https://m.media-amazon.com/images/M/MV5BODIyMDdhNTgtNDlmOC00MjUxLWE2NDItODA5MTdkNzY3ZTdhXkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="The Batman" year="2022" imdbID="tt1877830" type="movie" poster="https://m.media-amazon.com/images/M/MV5BMmU5NGJlMzAtMGNmOC00YjJjLTgyMzUtNjAyYmE4Njg5YWMyXkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="Batman v Superman: Dawn of Justice" year="2016" imdbID="tt2975590" type="movie" poster="https://m.media-amazon.com/images/M/MV5BZTJkYjdmYjYtOGMyNC00ZGU1LThkY2ItYTc1OTVlMmE2YWY1XkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="Batman v Superman: Dawn of Justice" year="2016" imdbID="tt2975590" type="movie" poster="https://m.media-amazon.com/images/M/MV5BZTJkYjdmYjYtOGMyNC00ZGU1LThkY2ItYTc1OTVlMmE2YWY1XkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="Batman" year="1989" imdbID="tt0096895" type="movie" poster="https://m.media-amazon.com/images/M/MV5BYzZmZWViM2EtNzhlMi00NzBlLWE0MWEtZDFjMjk3YjIyNTBhXkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="Batman Returns" year="1992" imdbID="tt0103776" type="movie" poster="https://m.media-amazon.com/images/M/MV5BZTliMDVkYTktZDdlMS00NTAwLWJhNzYtMWIwMDZjN2ViMGFiXkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="Batman & Robin" year="1997" imdbID="tt0118688" type="movie" poster="https://m.media-amazon.com/images/M/MV5BYzU3ZjE3M2UtM2E4Ni00MDI5LTkyZGUtOTFkMGIyYjNjZGU3XkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="Batman Forever" year="1995" imdbID="tt0112462" type="movie" poster="https://m.media-amazon.com/images/M/MV5BMTUyNjJhZWItMTZkNS00NDc4LTllNjUtYTg3NjczMzA5ZTViXkEyXkFqcGc@._V1_SX300.jpg"/>
  <result title="The Lego Batman Movie" year="2017" imdbID="tt4116284" type="movie" poster="https://m.media-amazon.com/images/M/MV5BMTcyNTEyOTY0M15BMl5BanBnXkFtZTgwOTAyNzU3MDI@._V1_SX300.jpg"/>
  <result title="Batman v Superman: Dawn of Justice (Ultimate Edition)" year="2016" imdbID="tt18689424" type="movie" poster="https://m.media-amazon.com/images/M/MV5BOTRlNWQwM2ItNjkyZC00MGI3LThkYjktZmE5N2FlMzcyNTIyXkEyXkFqcGdeQXVyMTEyNzgwMDUw._V1_SX300.jpg"/>
</root>

XML dependencies

<dependencies>
    <dependency>
        <groupId>jakarta.xml.bind</groupId>
        <artifactId>jakarta.xml.bind-api</artifactId>
        <version>4.0.4</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>4.0.6</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.20.0</version>
    </dependency>
</dependencies>

XML Model Classes

  • Create an "xml" package inside the "model" package.
  • Create a class called "MovieSearchResult".
  • The structure of the XML requires two classes: one for the root element and another for the repeating <result> elements.
    • This class handles the result elements.
    • These are annotated for either JAXB or Jackson.
package edu.kirkwood.model.xml;

import jakarta.xml.bind.annotation.*; // For JAXB
// import com.fasterxml.jackson.data.format.xml.annotation.*; // For Jackson

@XmlAccessorType(XmlAccessType.FIELD) // Tells JAXB to use fields directly
public class MovieSearchResult {

    @XmlAttribute(name = "title")
    private String title;

    @XmlAttribute(name = "year")
    private String year;

    @XmlAttribute(name = "imdbID")
    private String imdbID;

    @XmlAttribute(name = "type")
    private String type;

    @XmlAttribute(name = "poster")
    private String poster;

    public String getTitle() {
        return title;
    }

    public String getYear() {
        return year;
    }

    public String getImdbID() {
        return imdbID;
    }

    public String getType() {
        return type;
    }

    public String getPoster() {
        return poster;
    }

    @Override
    public String toString() {
        return "MovieSearchResult{" +
                "title='" + title + '\'' +
                ", year='" + year + '\'' +
                ", imdbID='" + imdbID + '\'' +
                ", type='" + type + '\'' +
                ", poster='" + poster + '\'' +
                '}';
    }
}

XML Model Classes

  • Create a class called "OmdbMovieResponse".
  • The structure of the XML requires two classes: one for the root element and another for the repeating <result> elements.
    • This class handles the root element.
    • These are annotated for either JAXB or Jackson.
package edu.kirkwood.model.xml;

import jakarta.xml.bind.annotation.*; // For JAXB
// import com.fasterxml.jackson.data.format.xml.annotation.*; // For Jackson

import java.util.List;

@XmlRootElement(name = "root")
@XmlAccessorType(XmlAccessType.FIELD) // Tells JAXB to use fields directly
public class OmdbMovieResponse {

    @XmlElement(name = "result")
    private List<MovieSearchResult> searchResults;

    @XmlAttribute(name = "totalResults")
    private int totalResults;

    @XmlAttribute(name = "response")
    private String response;

    public List<MovieSearchResult> getSearchResults() {
        return searchResults;
    }

    public int getTotalResults() {
        return totalResults;
    }

    public String getResponse() {
        return response;
    }
}

MovieDAO interface

  • Create a "edu.kirkwood.dao" package.
  • Create an interface called "MovieDAO". Add the following code.
package edu.kirkwood.dao;

import edu.kirkwood.model.Movie;
import java.util.List;
import java.util.Optional;

/**
 * The data access interface for movies.
 * This contract defines the operations for any data source.
 */
public interface MovieDAO {

    /**
     * Finds a single movie by its unique ID.
     * @param id The ID of the movie to find.
     * @return The movie if found, otherwise empty.
     */
    Movie findById(String id);

    /**
     * Retrieves all movies from the data source that match the title.
     * @param title
     * @return A list of all movies.
     */
    List<Movie> search(String title);
}

MovieDAOXml class

  • Next, create a separate class for each data source that implements your MovieDAO interface.
  • Create a package called "impl" inside "dao" for implementation. Create a class called MovieDAOXml.
package edu.kirkwood.dao.impl;

import edu.kirkwood.dao.MovieDAO;
import edu.kirkwood.model.Movie;
import java.util.List;
import java.util.Optional;

public class XmlMovieDAO implements MovieDAO {
    private final String url;

    public XmlMovieDAO(String url) {
        this.url = url;
    }

    /**
     * Logic to parse the XML file and return the movie matching the ID.
     * @param id The ID of the movie to find.
     * @return The movie if found, otherwise empty.
     */
    @Override
    public Movie findById(String id) {
        System.out.println("Fetching movie " + id + " from XML...");
        return null;
    }

    /**
     * Logic to parse the entire XML file and return a list of all movies.
     * @param title
     * @return A list of all movies.
     */
    @Override
    public List<Movie> search(String title) {
        System.out.println("Searching for " + title + " from XML...");
        return List.of();
    }
}

MovieDAO interface

  • Update the "MovieDAO" interface from the previous class with the following code.
package edu.kirkwood.dao;

import edu.kirkwood.model.xml.MovieSearchResult;

import java.util.List;

/**
 * The data access interface for movies.
 * This contract defines the operations for any data source.
 */
public interface MovieDAO<T> {
    
    /**
     * Retrieves all movies from the data source that match the title.
     * @param title
     * @return A list of all movies.
     */
    List<MovieSearchResult> search(String title);
}

XmlMovieDAO class

  • Next, create a separate class for each data source that implements your MovieDAO interface.
  • Create a package called "impl" inside "dao" for implementation. Create a class called MovieDAOXml.
package edu.kirkwood.dao.impl;

import edu.kirkwood.dao.MovieDAO;

import edu.kirkwood.model.xml.MovieSearchResult;
import edu.kirkwood.model.xml.OmdbMovieResponse;

import jakarta.xml.bind.JAXBContext; // JAXB
import jakarta.xml.bind.Unmarshaller; // JAXB

import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;

public class XmlMovieDAO implements MovieDAO {
    private String apiURL = null;

    public XmlMovieDAO(String apiURL) {
        this.apiURL = apiURL;
    }
}

XmlMovieDAO class

  • Add a search method.
package edu.kirkwood.dao.impl;

import edu.kirkwood.dao.MovieDAO;

import edu.kirkwood.model.xml.MovieSearchResult;
import edu.kirkwood.model.xml.OmdbMovieResponse;

import jakarta.xml.bind.JAXBContext; // JAXB
import jakarta.xml.bind.Unmarshaller; // JAXB

import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;

public class XmlMovieDAO implements MovieDAO {
    private String apiURL = null;

    public XmlMovieDAO(String apiURL) {
        this.apiURL = apiURL;
    }
    
    /**
     * Finds movies by a search term.
     * @param searchTerm The movie title to search
     * @return List<MovieSearchResult> the list of movies
     */
    public List<MovieSearchResult> search(String searchTerm) {
        if(apiURL == null) {
            throw new IllegalArgumentException("XML API URL is not configured in application.properties");
        }
        // URL-encode the search term to handle spaces and special characters
        String encodedSearchTerm = URLEncoder.encode(searchTerm, StandardCharsets.UTF_8);
        apiURL = String.format("%s&s=%s&page=1", apiURL, encodedSearchTerm);

        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(apiURL))
                    .build();
            HttpClient httpClient = HttpClient.newHttpClient();
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

            return Collections.emptyList();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Collections.emptyList();
    }
}

XmlMovieDAO class

  • Add a parseXml method.
package edu.kirkwood.dao.impl;

import edu.kirkwood.dao.MovieDAO;

import edu.kirkwood.model.xml.MovieSearchResult;
import edu.kirkwood.model.xml.OmdbMovieResponse;

import jakarta.xml.bind.JAXBContext; // JAXB
import jakarta.xml.bind.Unmarshaller; // JAXB

import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;

public class XmlMovieDAO implements MovieDAO {
    private String apiURL = null;

    public XmlMovieDAO(String apiURL) {
        this.apiURL = apiURL;
    }
    
    /**
     * Finds movies by a search term.
     * @param searchTerm The movie title to search
     * @return List<MovieSearchResult> the list of movies
     */
    public List<MovieSearchResult> search(String searchTerm) {
        if(apiURL == null) {
            throw new IllegalArgumentException("XML API URL is not configured in application.properties");
        }
        // URL-encode the search term to handle spaces and special characters
        String encodedSearchTerm = URLEncoder.encode(searchTerm, StandardCharsets.UTF_8);
        apiURL = String.format("%s&s=%s&page=1", apiURL, encodedSearchTerm);

        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(apiURL))
                    .build();
            HttpClient httpClient = HttpClient.newHttpClient();
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

            return parseXml(response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Collections.emptyList();
    }
    
    /**
     * Logic to parse the entire XML file and return a list of all movies.
     * @param xml The raw XML data
     * @return List<MovieSearchResult> the list of movies
     */
    private List<MovieSearchResult> parseXml(String xml) throws Exception {
        JAXBContext context = JAXBContext.newInstance(OmdbMovieResponse.class);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        StringReader reader = new StringReader(xml);
        OmdbMovieResponse movieResponse = (OmdbMovieResponse) unmarshaller.unmarshal(reader);
        return movieResponse.getSearchResults() != null ? movieResponse.getSearchResults() : Collections.emptyList();
    }
}

Application Properties

  • Create a properties file in your resources folder (src/main/resources/application.properties)
  • Set the desired data source. (XML, JSON, MYSQL, MONGODB), the API key, and parser preference (JAXB, JACKSON)

    • My API key is 359a6530 in case you don't want to sign up for a key.

datasource.type=XML
xml.apiURL=https://www.omdbapi.com/?r=xml&type=movie&apikey=<your-api-key>

The Factory

  • To efficiently switch data sources, you can use a Factory pattern.
  • Now, create a class called "MovieDAOFactory" in the "dao" package to read the application.properties file.
  • This class reads a configuration setting and provides the correct DAO implementation. 
  • This line creates a single static Properties object named props.
    • Is is similar to a Python dictionary or Java map, designed to hold key-value pairs from the .properties file.
package edu.kirkwood.dao;

import edu.kirkwood.dao.impl.*;

import java.io.InputStream;
import java.util.Properties;

public class MovieDAOFactory {

    private static final Properties props = new Properties();

}

The Factory

  • A static { ... } block is a code block that only runs only once, the very first time the class is loaded.

  • This example finds the application.properties file and initializes the static props variable.

  • InputStream is AutoCloseable, so it can be written in a try-with-resources block.

  • The getClassLoader() method returns the class loader. The ClassLoader has a method called getResourceAsStream()

package edu.kirkwood.dao;

import edu.kirkwood.dao.impl.*;

import java.io.InputStream;
import java.util.Properties;

public class MovieDAOFactory {

    private static final Properties props = new Properties();

    static {
        try (InputStream input = MovieDAOFactory.class.getClassLoader()
        											  .getResourceAsStream("application.properties")) {
            
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

The Factory

  • If the InputStream is null, that means the properties file cannot be found.

  • If the properties file is found, we pass the InputStream object to the load() method of the Properties class.

package edu.kirkwood.dao;

import edu.kirkwood.dao.impl.*;

import java.io.InputStream;
import java.util.Properties;

public class MovieDAOFactory {

    private static final Properties props = new Properties();

    static {
        try (InputStream input = MovieDAOFactory.class.getClassLoader()
        											  .getResourceAsStream("application.properties")) {
            if (input == null) {
                System.out.println("Unable to find application.properties");
            }
            props.load(input);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

The Factory

  • Create a method to get the DAO specified in the properties file.
  •  
package edu.kirkwood.dao;

import edu.kirkwood.dao.impl.*;

import java.io.InputStream;
import java.util.Properties;

public class MovieDAOFactory {

    private static final Properties props = new Properties();

    static {
        try (InputStream input = MovieDAOFactory.class.getClassLoader().getResourceAsStream("application.properties")) {
            if (input == null) {
                System.out.println("Unable to find application.properties");
            }
            props.load(input);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public static MovieDAO getMovieDAO() {
        String sourceType = props.getProperty("datasource.type");

        switch (sourceType.toUpperCase()) {
            case "XML":
                break;

            // other cases for JSON, MYSQL, MongoDB, etc.

            default:
                throw new IllegalArgumentException("Invalid data source type specified: " + sourceType);
        }
    }
}

The Factory

  • Now, create the factory class to read the application.properties file.
  • ]Create a "MovieDAOFactory" class in the "dao" package.
package edu.kirkwood.dao;

import edu.kirkwood.dao.impl.*;

import java.io.InputStream;
import java.util.Properties;

public class MovieDAOFactory {

    private static final Properties props = new Properties();

    static {
        try (InputStream input = MovieDAOFactory.class.getClassLoader().getResourceAsStream("application.properties")) {
            if (input == null) {
                System.out.println("Unable to find application.properties");
            }
            props.load(input);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public static MovieDAO getMovieDAO() {
        String sourceType = props.getProperty("datasource.type");

        switch (sourceType.toUpperCase()) {
            case "XML":
            	String apiURL = props.getProperty("xml.apiURL");
                if (apiURL == null) {
                    throw new IllegalArgumentException("XML API URL is not configured in application.properties");
                }
                return new XmlMovieDAO(apiURL);

            // other cases for JSON, MYSQL, MongoDB, etc.

            default:
                throw new IllegalArgumentException("Invalid data source type specified: " + sourceType);
        }
    }
}

Movie App Main class

  • Create a "MovieApp" class containing a main method.
  • Your main application logic can now get the MovieDAOXml and call the new method.
package edu.kirkwood;

import edu.kirkwood.dao.MovieDAO;
import edu.kirkwood.dao.MovieDAOFactory;
import edu.kirkwood.dao.impl.MovieDAOXml;
import edu.kirkwood.model.xml.MovieSearchResult;

import java.util.List;

public class MovieApp {
    public static void main(String[] args) {
        MovieDAO movieDAO = MovieDAOFactory.getMovieDAO();

        String search = "Batman";

        if (movieDAO instanceof XmlMovieDAO) {
            XmlMovieDAO xmlMovieDao = (XmlMovieDAO) movieDAO;
            List<MovieSearchResult> results = xmlMovieDao.search(search);

            System.out.println("Found " + results.size() + " results for '" + search + "':");
            results.forEach(System.out::println);
        }
    }
}

Java 2 - Week 6

By Marc Hauschildt

Java 2 - Week 6

  • 107