/* * Copyright (c) 2008 Los Alamos National Security, LLC. * * Los Alamos National Laboratory Research Library Digital Library Research & * Prototyping Team * * This library 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 2.1 of the License, or (at your option) * any later version. * * This library 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package gov.lanl.adore.djatoka.openurl; import gov.lanl.adore.djatoka.DjatokaDecodeParam; import gov.lanl.adore.djatoka.DjatokaException; import gov.lanl.adore.djatoka.DjatokaExtractProcessor; import gov.lanl.adore.djatoka.io.FormatConstants; import gov.lanl.adore.djatoka.kdu.KduExtractExe; import gov.lanl.adore.djatoka.plugin.ITransformPlugIn; import gov.lanl.adore.djatoka.util.IOUtils; import gov.lanl.adore.djatoka.util.ImageRecord; import gov.lanl.util.HttpDate; import info.freelibrary.djatoka.util.CacheUtils; import info.openurl.oom.ContextObject; import info.openurl.oom.OpenURLRequest; import info.openurl.oom.OpenURLRequestProcessor; import info.openurl.oom.OpenURLResponse; import info.openurl.oom.Service; import info.openurl.oom.config.ClassConfig; import info.openurl.oom.config.OpenURLConfig; import info.openurl.oom.entities.Referent; import info.openurl.oom.entities.ServiceType; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.security.MessageDigest; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.servlet.http.HttpServletResponse; import org.oclc.oomRef.descriptors.ByValueMetadataImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The OpenURLJP2KService OpenURL Service * * @author Ryan Chute */ public class OpenURLJP2KService implements Service, FormatConstants { private static Logger LOGGER = LoggerFactory .getLogger(OpenURLJP2KService.class); private static final String DEFAULT_IMPL_CLASS = SimpleListResolver.class .getCanonicalName(); private static final String PROPS_REQUESTER = "requester"; private static final String PROPS_REFERRING_ENTITY = "referringEntity"; private static final String PROPS_KEY_IMPL_CLASS = "OpenURLJP2KService.referentResolverImpl"; private static final String PROPS_KEY_CACHE_ENABLED = "OpenURLJP2KService.cacheEnabled"; private static final String PROPS_KEY_CACHE_TMPDIR = "OpenURLJP2KService.cacheTmpDir"; private static final String PROPS_KEY_TRANSFORM = "OpenURLJP2KService.transformPlugin"; private static final String PROPS_KEY_CACHE_SIZE = "OpenURLJP2KService.cacheSize"; private static final String PROP_KEY_CACHE_MAX_PIXELS = "OpenURLJP2KService.cacheImageMaxPixels"; private static final String SVC_ID = "info:lanl-repo/svc/getRegion"; private static final String DEFAULT_CACHE_SIZE = "1000"; private static final int DEFAULT_CACHE_MAXPIXELS = 100000; private static String implClass = null; private static Properties props = new Properties(); private static boolean init = false; private static boolean cacheTiles = true; private static boolean transformCheck = false; private static ITransformPlugIn transform; private static String cacheDir = null; private static TileCacheManager<String, String> tileCache; private static DjatokaExtractProcessor extractor; private static int maxPixels = DEFAULT_CACHE_MAXPIXELS; /** * Construct an info:lanl-repo/svc/getRegion web service class. Initializes * Referent Resolver instance using OpenURLJP2KService.referentResolverImpl * property. * * @param openURLConfig * OOM Properties forwarded from OpenURLServlet * @param classConfig * Implementation Properties forwarded from OpenURLServlet * @throws ResolverException */ public OpenURLJP2KService(OpenURLConfig openURLConfig, ClassConfig classConfig) throws ResolverException { try { if (!init) { props = IOUtils.loadConfigByCP(classConfig.getArg("props")); if (!ReferentManager.isInit()) { implClass = props.getProperty(PROPS_KEY_IMPL_CLASS, DEFAULT_IMPL_CLASS); ReferentManager.init( (IReferentResolver) Class.forName(implClass) .newInstance(), props); } cacheDir = props.getProperty(PROPS_KEY_CACHE_TMPDIR); if (props.getProperty(PROPS_KEY_CACHE_ENABLED) != null) cacheTiles = Boolean.parseBoolean(props .getProperty(PROPS_KEY_CACHE_ENABLED)); if (cacheTiles) { int cacheSize = Integer.parseInt(props.getProperty( PROPS_KEY_CACHE_SIZE, DEFAULT_CACHE_SIZE)); tileCache = new TileCacheManager<String, String>(cacheSize); } if (props.getProperty(PROPS_KEY_TRANSFORM) != null) { transformCheck = true; String transClass = props.getProperty(PROPS_KEY_TRANSFORM); transform = (ITransformPlugIn) Class.forName(transClass) .newInstance(); transform.setup(props); } if (props.getProperty(PROP_KEY_CACHE_MAX_PIXELS) != null) maxPixels = Integer.parseInt(props .getProperty(PROP_KEY_CACHE_MAX_PIXELS)); extractor = new DjatokaExtractProcessor(new KduExtractExe()); init = true; } } catch (IOException e) { LOGGER.error(e.getMessage(), e); throw new ResolverException( "Error attempting to open props file from classpath, disabling " + SVC_ID + " : " + e.getMessage()); } catch (Exception e) { LOGGER.error(e.getMessage(), e); throw new ResolverException( "Unable to inititalize implementation: " + props.getProperty(implClass) + " - " + e.getMessage()); } } /** * Returns the OpenURL service identifier for this implementation of * info.openurl.oom.Service */ public URI getServiceID() throws URISyntaxException { return new URI(SVC_ID); } public static boolean removeFromTileCache(String aCacheID) { return tileCache.remove(aCacheID) != null; } /** * Returns the OpenURLResponse consisting of an image bitstream to be * rendered on the client. Having obtained a result, this method is then * responsible for transforming it into an OpenURLResponse that acts as a * proxy for HttpServletResponse. */ public OpenURLResponse resolve(ServiceType serviceType, ContextObject contextObject, OpenURLRequest openURLRequest, OpenURLRequestProcessor processor) { String djatokaCacheFile = null; String responseFormat = null; String format = "image/jpeg"; int status = HttpServletResponse.SC_OK; HashMap<String, String> kev = setServiceValues(contextObject); DjatokaDecodeParam params = new DjatokaDecodeParam(); String id = null; if (kev.containsKey("region")) params.setRegion(kev.get("region")); if (kev.containsKey("format")) { format = kev.get("format"); if (!format.startsWith("image")) { // ignoring invalid format identifier format = "image/jpeg"; } } if (kev.containsKey("level")) params.setLevel(Integer.parseInt(kev.get("level"))); if (kev.containsKey("rotate")) params.setRotationDegree(Integer.parseInt(kev.get("rotate"))); if (kev.containsKey("scale")) { String[] v = kev.get("scale").split(","); if (v.length == 1) { if (v[0].contains(".")) params.setScalingFactor(Double.parseDouble(v[0])); else { int[] dims = new int[] { -1, Integer.parseInt(v[0]) }; params.setScalingDimensions(dims); } } else if (v.length == 2) { int[] dims = new int[] { Integer.parseInt(v[0]), Integer.parseInt(v[1]) }; params.setScalingDimensions(dims); } } if (kev.containsKey("clayer") && kev.get("clayer") != null) { int clayer = Integer.parseInt(kev.get("clayer")); if (clayer > 0) params.setCompositingLayer(clayer); } responseFormat = format; byte[] bytes = null; /* * DEAD CODE: responseFormat set right above -- TODO: remove if * (responseFormat == null) { try { bytes = * ("Output Format Not Supported").getBytes("UTF-8"); } catch * (UnsupportedEncodingException e) { e.printStackTrace(); } * responseFormat = "text/plain"; status = * HttpServletResponse.SC_NOT_FOUND; } else */ if (params.getRegion() != null && params.getRegion().contains("-")) { try { bytes = ("Negative Region Arguments are not supported.") .getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } responseFormat = "text/plain"; status = HttpServletResponse.SC_NOT_FOUND; } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Service has a valid region request"); } try { Referent referent = contextObject.getReferent(); ImageRecord r = ReferentManager.getImageRecord(referent); if (LOGGER.isDebugEnabled() && r != null) { LOGGER.debug("Retrieving ImageRecord for: {}", r.getIdentifier()); } if (r != null) { if (transformCheck && transform != null) { HashMap<String, String> instProps = new HashMap<String, String>(); if (r.getInstProps() != null) instProps.putAll(r.getInstProps()); if (contextObject.getRequesters().length > 0 && contextObject.getRequesters()[0] .getDescriptors().length > 0) { String requester = contextObject.getRequesters()[0] .getDescriptors()[0].toString(); instProps.put(PROPS_REQUESTER, requester); } if (contextObject.getReferringEntities().length > 0 && contextObject.getReferringEntities()[0] .getDescriptors().length > 0) instProps.put(PROPS_REFERRING_ENTITY, contextObject.getReferringEntities()[0] .getDescriptors()[0].toString()); if (instProps.size() > 0) transform.setInstanceProps(instProps); params.setTransform(transform); } if (!cacheTiles || !isCacheable(params)) { if (LOGGER.isWarnEnabled()) { LOGGER.warn("Not using the OpenURL layer cache"); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); extractor.extractImage(r.getImageFile(), baos, params, format); bytes = baos.toByteArray(); baos.close(); } else { String ext = getExtension(format); String hash = getTileHash(r, params); String file = tileCache.get(hash + ext); File f; id = r.getIdentifier(); if (file == null || (file != null && !(f = new File(file)).exists() && f .length() > 0)) { if (cacheDir != null) { File cacheDirFile = new File(cacheDir); // If our cache dir doesn't exist, create it if (!cacheDirFile.exists()) { cacheDirFile.mkdirs(); } f = File.createTempFile( "cache" + hash.hashCode() + "-", "." + ext, cacheDirFile); } else { f = File.createTempFile( "cache" + hash.hashCode() + "-", "." + ext); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Temp file created: {}", f); } f.deleteOnExit(); file = f.getAbsolutePath(); djatokaCacheFile = file; extractor.extractImage(r.getImageFile(), file, params, format); if (tileCache.get(hash + ext) == null) { tileCache.put(hash + ext, file); bytes = IOUtils.getBytesFromFile(f); if (LOGGER.isDebugEnabled()) { LOGGER.debug("makingTile: " + file + " " + bytes.length + " params: " + params); } } else { // Handles simultaneous request on separate // thread, ignores cache. bytes = IOUtils.getBytesFromFile(f); f.delete(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("tempTile: " + file + " " + bytes.length + " params: " + params); } } } else { bytes = IOUtils.getBytesFromFile(new File(file)); if (LOGGER.isDebugEnabled()) { LOGGER.debug("tileCache: " + file + " " + bytes.length); } djatokaCacheFile = file; } } } else if (LOGGER.isWarnEnabled()) { LOGGER.warn("Unable to retrieve ImageRecord"); } } catch (ResolverException e) { LOGGER.error(e.getMessage(), e); bytes = e.getMessage().getBytes(); responseFormat = "text/plain"; status = HttpServletResponse.SC_NOT_FOUND; } catch (DjatokaException e) { LOGGER.error(e.getMessage(), e); bytes = e.getMessage().getBytes(); responseFormat = "text/plain"; status = HttpServletResponse.SC_NOT_FOUND; } catch (Exception e) { LOGGER.error(e.getMessage(), e); bytes = e.getMessage().getBytes(); responseFormat = "text/plain"; status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; } } if (bytes == null || bytes.length == 0) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("No bytes found!"); } bytes = "".getBytes(); responseFormat = "text/plain"; status = HttpServletResponse.SC_NOT_FOUND; } HashMap<String, String> header_map = new HashMap<String, String>(); header_map.put("Content-Length", bytes.length + ""); header_map.put("Date", HttpDate.getHttpDate()); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Getting OpenURLResponse..."); } OpenURLResponse response = new OpenURLResponse(status, responseFormat, bytes, header_map); // Record where our cache file was (if we had/created one) if (djatokaCacheFile != null) { int[] dims = params.getScalingDimensions(); String scale = dims != null ? Integer.toString(dims[1]) : ""; String level = Integer.toString(params.getLevel()); String region = params.getRegion(); String ext = getExtension(format); String hash; try { hash = getTileHash(id, params); } catch (Exception details) { if (LOGGER.isErrorEnabled()) { LOGGER.error(details.getMessage(), details); } hash = null; } id = id + "_" + CacheUtils.getFileName(level, scale, region); if (LOGGER.isDebugEnabled()) { LOGGER.debug("OpenURL service: [ {} | {} | {} ] = {}", new String[] { level, scale, region, id }); } if (hash != null) { Map sessionMap = response.getSessionMap(); String cacheName = hash + ext; if (LOGGER.isDebugEnabled()) { LOGGER.debug("Setting cache session data [ {} | {} ]", new String[] { id, djatokaCacheFile }); } sessionMap.put(id, djatokaCacheFile); sessionMap.put(djatokaCacheFile, cacheName); } } else if (LOGGER.isDebugEnabled()) { LOGGER.debug("djatokaCacheFile variable is null"); } return response; } private boolean isCacheable(DjatokaDecodeParam params) { if (transformCheck && params.getTransform().isTransformable()) return false; if (params.getScalingFactor() != 1.0) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("NOT CACHING BECAUSE SCALING FACTOR != 1.0"); } return false; } /* * Commented out below because we want to cache them all; we're working * with openseadragon and will have a fixed/limited number of derivative * files... * * if (params.getScalingDimensions() != null) { int[] sd = * params.getScalingDimensions(); if (sd.length == 1 && sd[0] >= * maxPixels/2) return false; if (sd.length == 2 && (sd[0] * sd[1]) >= * maxPixels) return false; } if (params.getRegion() != null) { String[] * r = params.getRegion().split(","); if (r.length == 4) { int h = * Integer.parseInt(r[2]); int w = Integer.parseInt(r[3]); if ((h * w) * >= maxPixels) return false; } } */ return true; } private static final String getTileHash(ImageRecord r, DjatokaDecodeParam params) throws Exception { return getTileHash(r.getIdentifier(), params); } private static final String getTileHash(String id, DjatokaDecodeParam params) throws Exception { int level = params.getLevel(); String region = params.getRegion(); int rotateDegree = params.getRotationDegree(); double scalingFactor = params.getScalingFactor(); int[] scalingDims = params.getScalingDimensions(); String scale = ""; if (scalingDims != null && scalingDims.length == 1) scale = scalingDims[0] + ""; if (scalingDims != null && scalingDims.length == 2) scale = scalingDims[0] + "," + scalingDims[1]; int clayer = params.getCompositingLayer(); String rft_id = id + "|" + level + "|" + region + "|" + rotateDegree + "|" + scalingFactor + "|" + scale + "|" + clayer; MessageDigest complete = MessageDigest.getInstance("SHA1"); return new String(complete.digest(rft_id.getBytes())); } private static final String getExtension(String mimetype) { if (mimetype.equals(FORMAT_MIMEYPE_JPEG)) return FORMAT_ID_JPG; if (mimetype.equals(FORMAT_MIMEYPE_PNG)) return FORMAT_ID_PNG; if (mimetype.equals(FORMAT_MIMEYPE_GIF)) return FORMAT_ID_GIF; if (mimetype.equals(FORMAT_MIMEYPE_PNM)) return FORMAT_ID_PNM; if (mimetype.equals(FORMAT_MIMEYPE_JP2)) return FORMAT_ID_JP2; if (mimetype.equals(FORMAT_MIMEYPE_JPX)) return FORMAT_ID_JPX; if (mimetype.equals(FORMAT_MIMEYPE_JPM)) return FORMAT_ID_JP2; return null; } private static HashMap<String, String> setServiceValues(ContextObject co) { HashMap<String, String> map = new HashMap<String, String>(); Object[] svcData = (Object[]) co.getServiceTypes()[0].getDescriptors(); if (svcData != null && svcData.length > 0) { for (int i = 0; i < svcData.length; i++) { Object tmp = svcData[i]; if (tmp.getClass().getSimpleName() .equals("ByValueMetadataImpl")) { ByValueMetadataImpl kev = ((ByValueMetadataImpl) tmp); if (kev.getFieldMap().size() > 0) { if (kev.getFieldMap().containsKey("svc.region") && ((String[]) kev.getFieldMap().get( "svc.region"))[0] != "") map.put("region", ((String[]) kev.getFieldMap() .get("svc.region"))[0]); if (kev.getFieldMap().containsKey("svc.format") && ((String[]) kev.getFieldMap().get( "svc.format"))[0] != "") map.put("format", ((String[]) kev.getFieldMap() .get("svc.format"))[0]); if (kev.getFieldMap().containsKey("svc.level") && ((String[]) kev.getFieldMap().get( "svc.level"))[0] != "") map.put("level", ((String[]) kev.getFieldMap().get( "svc.level"))[0]); if (kev.getFieldMap().containsKey("svc.rotate") && ((String[]) kev.getFieldMap().get( "svc.rotate"))[0] != "") map.put("rotate", ((String[]) kev.getFieldMap() .get("svc.rotate"))[0]); if (kev.getFieldMap().containsKey("svc.scale") && ((String[]) kev.getFieldMap().get( "svc.scale"))[0] != "") map.put("scale", ((String[]) kev.getFieldMap().get( "svc.scale"))[0]); if (kev.getFieldMap().containsKey("svc.clayer") && ((String[]) kev.getFieldMap().get( "svc.clayer"))[0] != "") map.put("clayer", ((String[]) kev.getFieldMap() .get("svc.clayer"))[0]); } } } } return map; } }