view iiif-presentation/src/main/java/digilib/servlet/Manifester.java @ 1652:f41cee9bc61b

Fix NPE when a file with an image extension is not readable as image.
author Robert Casties <r0bcas7@gmail.com>
date Tue, 07 Nov 2017 18:36:48 +0100
parents 03cc8daf39ea
children 53ba1e2ea3f6
line wrap: on
line source

package digilib.servlet;

/*
 * #%L
 * 
 * Manifester.java -- Servlet for creating IIIF Presentation API manifests.
 * 
 * Digital Image Library servlet components
 * %%
 * Copyright (C) 2003 - 2017 MPIWG Berlin
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 * Author: Robert Casties (robcast@sourceforge.net)
 * Created on 24.5.2017
 */

import java.io.IOException;
import java.util.EnumSet;
import java.util.List;

import javax.json.Json;
import javax.json.stream.JsonGenerator;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import digilib.auth.AuthOpException;
import digilib.auth.AuthzOps;
import digilib.conf.DigilibRequest.ParsingOption;
import digilib.conf.DigilibServletConfiguration;
import digilib.conf.DigilibServletRequest;
import digilib.conf.ManifestServletConfiguration;
import digilib.io.DocuDirCache;
import digilib.io.DocuDirectory;
import digilib.io.DocuDirent;
import digilib.io.FileOps;
import digilib.io.ImageFileSet;
import digilib.io.ImageInput;
import digilib.util.ImageSize;

/**
 * Servlet for creating IIIF Presentation API manifests.
 * 
 * 
 * @author casties
 * 
 */
public class Manifester extends HttpServlet {

	private static final long serialVersionUID = 6678666342141409868L;

	/** Servlet version */
	public static String mfVersion = ManifestServletConfiguration.getClassVersion();

	/** DigilibConfiguration instance */
	protected DigilibServletConfiguration dlConfig = null;

	/** general logger */
	protected Logger logger = Logger.getLogger("digilib.manifester");

	/** logger for accounting requests */
	protected static Logger accountlog = Logger.getLogger("account.manifester.request");

	/** AuthOps instance */
	protected AuthzOps authzOp;

	/** DocuDirCache instance */
	protected DocuDirCache dirCache;

	/** use authentication */
	protected boolean useAuthorization = false;
	
	/** scaler servlet path */
	protected String scalerServletPath;

	/** character for IIIF path separation */
	protected String iiifPathSep;

    /** set CORS header ACAO* for info requests */
    protected boolean corsForInfoRequests = true;

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);

		System.out.println("***** Digital Image Library IIF Manifest Servlet (version " + mfVersion + ") *****");

		// get our ServletContext
		ServletContext context = config.getServletContext();
		// see if there is a Configuration instance
		dlConfig = ManifestServletConfiguration.getCurrentConfig(context);
		if (dlConfig == null) {
			// no Configuration
			throw new ServletException("No Configuration!");
		}
		// say hello in the log file
		logger.info("***** Digital Image Library IIIF Manifest Servlet (version " + mfVersion + ") *****");

		// set our AuthOps
		useAuthorization = dlConfig.getAsBoolean("use-authorization");
		authzOp = (AuthzOps) dlConfig.getValue(DigilibServletConfiguration.AUTHZ_OP_KEY);
		// DocuDirCache instance
		dirCache = (DocuDirCache) dlConfig.getValue(DigilibServletConfiguration.DIR_CACHE_KEY);
		// Scaler path
		scalerServletPath = dlConfig.getAsString("scaler-servlet-name");
		// IIIF path separator
		iiifPathSep = dlConfig.getAsString("iiif-slash-replacement");
		// CORS for info requests
		corsForInfoRequests = dlConfig.getAsBoolean("iiif-info-cors");
	}

    /**
     * Returns modification time relevant to the request for caching.
     * 
     * @see javax.servlet.http.HttpServlet#getLastModified(javax.servlet.http.HttpServletRequest)
     */
    public long getLastModified(HttpServletRequest request) {
        accountlog.debug("GetLastModified from " + request.getRemoteAddr() + " for " + request.getQueryString());
        long mtime = -1;
        try {
            // create new digilib request
			DigilibServletRequest dlRequest = new DigilibServletRequest(request, dlConfig,
					EnumSet.of(ParsingOption.omitIiifImageApi));
			// get list of IIIF parameters
			@SuppressWarnings("unchecked")
			List<String> iiifParams = (List<String>) dlRequest.getValue("request.iiif.elements");
			// get identifier (first parameter)
			String identifier = iiifParams.get(0);
			// decode identifier to file path
			dlRequest.setValueFromString("fn", dlRequest.decodeIiifIdentifier(identifier));
            DocuDirectory dd = dirCache.getDirectory(dlRequest.getFilePath());
            if (dd != null) {
                mtime = dd.getDirMTime() / 1000 * 1000;
            }
        } catch (Exception e) {
            logger.error("error in getLastModified: " + e.getMessage());
        }
        logger.debug("  returns " + mtime);
        return mtime;
    }

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.
	 * HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		accountlog.info("GET from " + request.getRemoteAddr());
		// do the processing
		processRequest(request, response);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.
	 * HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		accountlog.info("POST from " + request.getRemoteAddr());
		// do the processing
		processRequest(request, response);
	}

	protected void processRequest(HttpServletRequest request, HttpServletResponse response) {
		try {
			// create DigilibRequest from ServletRequest, omit IIIF Image API parsing
			DigilibServletRequest dlRequest = new DigilibServletRequest(request, dlConfig,
					EnumSet.of(ParsingOption.omitIiifImageApi));
			// get list of IIIF parameters
			@SuppressWarnings("unchecked")
			List<String> iiifParams = (List<String>) dlRequest.getValue("request.iiif.elements");
			if (iiifParams == null) {
				logger.error("Invalid IIIF request.");
				response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid IIIF request.");
				return;
			}
			// get identifier (first parameter)
			String identifier = ""; // allow empty identifier for image root dir
			if (iiifParams.size() > 0) {			
				identifier = iiifParams.get(0);
			}
			// decode identifier to file path
			dlRequest.setValueFromString("fn", dlRequest.decodeIiifIdentifier(identifier));
			// get directory path
			String dlFn = dlRequest.getFilePath();
			// get information about the directory
			DocuDirectory dlDir = dirCache.getDirectory(dlFn);
			if (dlDir == null) {
				logger.error("Directory for manifest not found: " + dlFn);
				response.sendError(HttpServletResponse.SC_NOT_FOUND);
				return;
			}
            if (dlDir.size() == 0) {
                logger.debug("Directory has no files: " + dlFn);
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
            }

            ManifestParams params = new ManifestParams();

            /*
			 * set CORS header ACAO "*" for info response as per IIIF spec
			 */
			if (corsForInfoRequests) {
				String origin = request.getHeader("Origin");
				if (origin != null) {
					response.setHeader("Access-Control-Allow-Origin", "*");
				}
			}

            /*
             * check permissions
             */
            if (useAuthorization) {
                // is the current request/user authorized?
                if (!authzOp.isAuthorized(dlRequest)) {
                	// TODO: does this work for directories?
                    // send deny answer and abort
                    throw new AuthOpException("Access denied!");
                }
            }

			// use JSON-LD content type only when asked
			String accept = request.getHeader("Accept");
			if (accept != null && accept.contains("application/ld+json")) {
				response.setContentType("application/ld+json");
			} else {
				response.setContentType("application/json");
			}

			/*
			 * configure base URLs for manifest
			 */
			params.imgApiUrl = dlConfig.getAsString("iiif-image-base-url");
			String manifestBaseUrl = dlConfig.getAsString("iiif-manifest-base-url");
			if ("".equals(params.imgApiUrl) || "".equals(manifestBaseUrl)) {
				// try to figure out base URLs
				String servletBaseUrl = dlConfig.getAsString("webapp-base-url");
				if ("".equals(servletBaseUrl)) {
					String url = request.getRequestURL().toString();
					// get base URL for web application by last occurrence of Servlet path
					int srvPathLen = url.lastIndexOf(request.getServletPath());
					servletBaseUrl = url.substring(0, srvPathLen);
				}
				// manifest base URL
				manifestBaseUrl = servletBaseUrl + request.getServletPath() + "/" + dlConfig.getAsString("iiif-prefix");
				// Image API base URL
				params.imgApiUrl = servletBaseUrl + "/" + this.scalerServletPath + "/"
						+ dlConfig.getAsString("iiif-prefix");
			}
			// full manifest URL with identifier
			params.manifestUrl = manifestBaseUrl + "/" + identifier;
			params.identifier = identifier;
			params.docuDir = dlDir;
			
			/*
			 * start json representation
			 */
			ServletOutputStream out = response.getOutputStream();
			JsonGenerator manifest = Json.createGenerator(out).writeStartObject();
			/*
			 * manifest metadata
			 */
			writeManifestMeta(manifest, dlFn, params);
			
			/*
			 * sequences
			 */
			writeSequences(manifest, params);
			
			manifest.writeEnd(); // manifest
			manifest.close();

		} catch (IOException e) {
			logger.error("ERROR sending manifest: ", e);
		} catch (AuthOpException e) {
			logger.debug("Permission denied.");
			try {
				response.sendError(HttpServletResponse.SC_FORBIDDEN);
			} catch (IOException e1) {
				logger.error("Error sending error: ", e);
			}
		}
	}

    /**
     * @param manifest
     * @param dlFn
     * @param params 
     */
    protected void writeManifestMeta(JsonGenerator manifest, String dlFn, ManifestParams params) {
        manifest.write("@context", "http://iiif.io/api/presentation/2/context.json")
            .write("@type", "sc:Manifest")
            .write("@id", params.manifestUrl + "/manifest")
            .write("label", "[Scanned work " + dlFn + "]")
            .write("description", "[Automatically generated manifest for scanned work " + dlFn + "]");
    }

    /**
     * @param dlDir
     * @param url
     * @param manifest
     * @param servletBaseUrl 
     */
    protected void writeSequences(JsonGenerator manifest, ManifestParams params) {
        manifest.writeStartArray("sequences");
        /*
         * first sequence
         */
        writeSequence(manifest, params);
        
        manifest.writeEnd(); // sequences
    }

	/**
	 * @param dlDir
	 * @param url
	 * @param manifest
	 * @param servletUrl 
	 */
	protected void writeSequence(JsonGenerator manifest, ManifestParams params) {
		manifest.writeStartObject()
			.write("@id", params.manifestUrl + "/sequence/default")
			.write("@type", "sc:Sequence")
			.write("label", "Scan image order");
		/*
		 * canvases
		 */
		writeCanvases(manifest, params);
		
		manifest.writeEnd(); // sequence
	}

	/**
	 * @param dlDir
	 * @param url
	 * @param manifest
	 * @param servletUrl 
	 */
	protected void writeCanvases(JsonGenerator manifest, ManifestParams params) {
		/*
		 * list of canvases
		 */
		manifest.writeStartArray("canvases");
		
		int idx = 0;
		for (DocuDirent imgFile : params.docuDir) {
			idx += 1;
			ImageFileSet imgFs = (ImageFileSet) imgFile;
			ImageInput img = imgFs.getBiggest();
			ImageSize imgSize = img.getSize();
			if (imgSize == null) continue;
			/*
			 * canvas
			 */
			writeCanvas(manifest, idx, imgFile, imgSize, params);
		}
		
		manifest.writeEnd(); // canvases
	}

	/**
	 * @param url
	 * @param manifest
	 * @param idx
	 * @param imgFile
	 * @param imgSize
	 * @param servletUrl 
	 */
    protected void writeCanvas(JsonGenerator manifest, int idx, DocuDirent imgFile, ImageSize imgSize,
            ManifestParams params) {
        manifest.writeStartObject()
            .write("@type", "sc:Canvas")
            .write("@id", params.manifestUrl + "/canvas/p" + idx)
            .write("label", "image " + FileOps.basename(imgFile.getName()))
            .write("height", imgSize.getHeight())
            .write("width", imgSize.getWidth());
        /*
         * images
         */
        writeImages(manifest, idx, imgFile, imgSize, params);

        manifest.writeEnd(); // canvas
    }

	/**
	 * @param url
	 * @param manifest
	 * @param idx
	 * @param imgFile
	 * @param imgSize
	 * @param servletUrl 
	 */
    protected void writeImages(JsonGenerator manifest, int idx, DocuDirent imgFile, ImageSize imgSize,
            ManifestParams params) {
        /*
         * list of images (just one)
         */
        manifest.writeStartArray("images");
        /*
         * image
         */
        writeImage(manifest, idx, imgFile, imgSize, params);

        manifest.writeEnd(); // images
    }

	/**
	 * @param url
	 * @param manifest
	 * @param idx
	 * @param imgFile
	 * @param imgSize
	 * @param servletUrl 
	 */
    protected void writeImage(JsonGenerator manifest, int idx, DocuDirent imgFile, ImageSize imgSize,
            ManifestParams params) {
        /*
         * image
         */        
        manifest.writeStartObject()
            .write("@type", "oa:Annotation")
            .write("@id", params.manifestUrl + "/annotation/p" + idx + "-image")
            .write("motivation", "sc:painting");
        /*
         * resource
         */
        writeResource(manifest, imgFile, imgSize, params);

        manifest.write("on", params.manifestUrl + "/canvas/p" + idx)
            .writeEnd(); // image
    }

	/**
	 * @param url
	 * @param manifest
	 * @param imgFile
	 * @param imgSize
	 * @param servletUrl 
	 */
    protected void writeResource(JsonGenerator manifest, DocuDirent imgFile, ImageSize imgSize,
            ManifestParams params) {
        // base URL for image using IIIF image API
        String iiifImgBaseUrl = params.imgApiUrl + "/" + params.identifier + this.iiifPathSep + FileOps.basename(imgFile.getName());
        // IIIF image parameters
        String imgUrl = iiifImgBaseUrl + "/full/full/0/default.jpg";
        /*
         * resource
         */
		manifest.writeStartObject("resource")
			.write("@id", imgUrl)
			.write("@type", "dctypes:Image")
			.write("format", "image/jpeg")
		    .write("height", imgSize.getHeight())
			.write("width", imgSize.getWidth());
        /*
         * (iiif) service
         */
        writeService(manifest, iiifImgBaseUrl, imgSize, params);
        
        manifest.writeEnd(); // resource
	}

	/**
	 * @param manifest
	 * @param iiifImgBaseUrl 
	 * @param imgSize 
	 * @param servletUrl 
	 */
    protected void writeService(JsonGenerator manifest, String iiifImgBaseUrl, ImageSize imgSize,
            ManifestParams params) {
	    /*
	     * service
	     */
		manifest.writeStartObject("service")
			.write("@context", "http://iiif.io/api/image/2/context.json")
			.write("@id", iiifImgBaseUrl)
			.write("profile", "http://iiif.io/api/image/2/profiles/level2.json")
			// maximum size
            .write("height", imgSize.getHeight())
            .write("width", imgSize.getWidth())
            /* other sizes
            .writeStartArray("sizes")
            .writeStartObject()
            .write("width", 100)
            .write("height", 100)
            .writeEnd() // size
            .writeEnd() // sizes
            */
            
			.writeEnd(); // service
	}

	/**
	 * Class holding parameters to construct manifest.
	 * @author casties
	 *
	 */
	protected class ManifestParams {
	    public DocuDirectory docuDir;
        String manifestUrl;
	    String imgApiUrl;
	    String identifier;	    
	}
}