Java tutorial
/** * Copyright (c) Codice Foundation * * <p>This 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 any later version. * * <p>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 Lesser General Public License for more details. A copy of the GNU Lesser General Public * License is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.alliance.plugin.nitf; import static org.apache.commons.lang3.Validate.notNull; import com.github.jaiimageio.jpeg2000.J2KImageWriteParam; import com.github.jaiimageio.jpeg2000.impl.J2KImageReaderSpi; import com.github.jaiimageio.jpeg2000.impl.J2KImageWriter; import com.github.jaiimageio.jpeg2000.impl.J2KImageWriterSpi; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteSource; import ddf.catalog.data.Attribute; import ddf.catalog.data.Metacard; import ddf.catalog.data.impl.AttributeImpl; import ddf.catalog.data.types.Core; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Semaphore; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.activation.MimeType; import javax.activation.MimeTypeParseException; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.spi.IIORegistry; import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream; import net.coobird.thumbnailator.Thumbnails; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.codice.ddf.catalog.async.data.api.internal.ProcessCreateItem; import org.codice.ddf.catalog.async.data.api.internal.ProcessDeleteItem; import org.codice.ddf.catalog.async.data.api.internal.ProcessRequest; import org.codice.ddf.catalog.async.data.api.internal.ProcessResource; import org.codice.ddf.catalog.async.data.api.internal.ProcessUpdateItem; import org.codice.ddf.catalog.async.data.impl.ProcessCreateItemImpl; import org.codice.ddf.catalog.async.data.impl.ProcessResourceImpl; import org.codice.ddf.catalog.async.data.impl.ProcessUpdateItemImpl; import org.codice.ddf.catalog.async.plugin.api.internal.PostProcessPlugin; import org.codice.ddf.platform.util.TemporaryFileBackedOutputStream; import org.codice.imaging.nitf.core.common.NitfFormatException; import org.codice.imaging.nitf.core.image.ImageSegment; import org.codice.imaging.nitf.fluent.NitfParserInputFlow; import org.codice.imaging.nitf.fluent.impl.NitfParserInputFlowImpl; import org.codice.imaging.nitf.render.NitfRenderer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This {@link PostProcessPlugin} creates and stores the NITF thumbnail, overview, and original * images. The thumbnail is stored in the metacard, and the overview and original are stored as * derived resources in the content store. */ public class NitfPostProcessPlugin implements PostProcessPlugin { private static final double DEFAULT_MAX_SIDE_LENGTH = 1024.0; private static final String IMAGE_NITF = "image/nitf"; @VisibleForTesting static final MimeType NITF_MIME_TYPE; static { IIORegistry.getDefaultInstance().registerServiceProvider(new J2KImageReaderSpi()); try { NITF_MIME_TYPE = new MimeType(IMAGE_NITF); } catch (MimeTypeParseException e) { throw new ExceptionInInitializerError( String.format("Unable to create MimeType from '%s': %s", IMAGE_NITF, e.getMessage())); } } private static final String IMAGE_JPEG = "image/jpeg"; private static final String IMAGE_JPEG2K = "image/jp2"; private static final int THUMBNAIL_WIDTH = 200; private static final int THUMBNAIL_HEIGHT = 200; private static final long BYTES_PER_MEGABYTE = 1024L * 1024L; private static final String JPG = "jpg"; private static final String JP2 = "jp2"; private static final Logger LOGGER = LoggerFactory.getLogger(NitfPostProcessPlugin.class); private static final String OVERVIEW = "overview"; private static final String ORIGINAL = "original"; private static final String DERIVED_IMAGE_FILENAME_PATTERN = "%s-%s.%s"; // non-word characters equivalent to [^a-zA-Z0-9_] private static final String INVALID_FILENAME_CHARACTER_REGEX = "[\\W]"; private static final int ARGB_COMPONENT_COUNT = 4; private static final int DEFAULT_MAX_NITF_SIZE_MB = 120; private static final int BYTES_PER_KILOBYTE = 1024; private static final int DEFAULT_FILE_BACKED_OUTPUT_STREAM_THRESHOLD = 32 * BYTES_PER_KILOBYTE; private volatile boolean createOverview = true; private volatile boolean storeOriginalImage = true; private volatile int maxNitfSizeMB = DEFAULT_MAX_NITF_SIZE_MB; private volatile double maxSideLength = DEFAULT_MAX_SIDE_LENGTH; private final Semaphore available = new Semaphore(2, true); private Supplier<NitfRenderer> nitfRendererSupplier; private Supplier<NitfParserInputFlow> nitfParserSupplier; public NitfPostProcessPlugin() { this(NitfRenderer::new, NitfParserInputFlowImpl::new); } public NitfPostProcessPlugin(Supplier<NitfRenderer> nitfRendererSupplier, Supplier<NitfParserInputFlow> nitfParserSupplier) { this.nitfRendererSupplier = nitfRendererSupplier; this.nitfParserSupplier = nitfParserSupplier; } @Override public ProcessRequest<ProcessCreateItem> processCreate(ProcessRequest<ProcessCreateItem> input) { handleProcessCreateItem( notNull(input, "processCreate(): argument 'input' may not be null.").getProcessItems()); return input; } @Override public ProcessRequest<ProcessUpdateItem> processUpdate(ProcessRequest<ProcessUpdateItem> input) { handleProcessUpdateItem( notNull(input, "processUpdate(): argument 'input' may not be null.").getProcessItems()); return input; } @Override public ProcessRequest<ProcessDeleteItem> processDelete(ProcessRequest<ProcessDeleteItem> input) { notNull(input, "processDelete(): argument 'input' may not be null."); return input; } private boolean isNitfMimeType(String rawMimeType) { try { return NITF_MIME_TYPE.match(rawMimeType); } catch (MimeTypeParseException e) { LOGGER.debug("unable to compare mime types: {} vs {}", NITF_MIME_TYPE, rawMimeType); } return false; } public void setMaxSideLength(int maxSideLength) { if (maxSideLength > 0) { LOGGER.trace("Setting derived image maxSideLength to {}", maxSideLength); this.maxSideLength = maxSideLength; } else { LOGGER.debug( "Invalid `maxSideLength` value [{}], must be greater than zero. Default value [{}] will be used instead.", maxSideLength, DEFAULT_MAX_SIDE_LENGTH); this.maxSideLength = DEFAULT_MAX_SIDE_LENGTH; } } public void setMaxNitfSizeMB(int maxNitfSizeMB) { this.maxNitfSizeMB = maxNitfSizeMB; } public void setCreateOverview(boolean createOverview) { this.createOverview = createOverview; } public void setStoreOriginalImage(boolean storeOriginalImage) { this.storeOriginalImage = storeOriginalImage; } private void handleProcessCreateItem(List<ProcessCreateItem> processCreateItems) { List<ProcessCreateItem> createItems = processCreateItems.stream().flatMap(this::handleProcessCreateItem) .collect(Collectors.toList()); processCreateItems.addAll(createItems); } private Stream<ProcessCreateItem> handleProcessCreateItem(ProcessCreateItem processCreateItem) { List<ProcessCreateItem> createdItems = new ArrayList<>(); Metacard metacard = processCreateItem.getMetacard(); ProcessResource processResource = processCreateItem.getProcessResource(); if (shouldProcess(processResource)) { try (TemporaryFileBackedOutputStream fbos = new TemporaryFileBackedOutputStream( DEFAULT_FILE_BACKED_OUTPUT_STREAM_THRESHOLD)) { // TODO: 06/21/2018 oconnormi - can probably get rid of this once // ProcessResourceImpl.getInputStream can be called multiple times fbos.write(IOUtils.toByteArray(processResource.getInputStream())); ByteSource byteSource = fbos.asByteSource(); BufferedImage renderedImage = renderImage(byteSource.openStream()); if (renderedImage != null) { addThumbnailToMetacard(metacard, renderedImage); processCreateItem.markMetacardAsModified(); if (createOverview) { ProcessResource overviewProcessResource = createOverviewResource(renderedImage, metacard); createdItems.add(new ProcessCreateItemImpl(overviewProcessResource, metacard)); } if (storeOriginalImage) { ProcessResource originalImageProcessResource = createOriginalImage( renderImageUsingOriginalDataModel(byteSource.openStream()), metacard); createdItems.add(new ProcessCreateItemImpl(originalImageProcessResource, metacard)); } } } catch (IOException | NitfFormatException | RuntimeException e) { LOGGER.debug("An error occured when rendering a nitf for {}", processResource.getName(), e); } catch (InterruptedException e) { LOGGER.error("Rendering failed for {}", processResource.getName(), e); Thread.currentThread().interrupt(); throw new RuntimeException(String.format("Rendering failed for %s", processResource.getName()), e); } } return createdItems.stream(); } private void handleProcessUpdateItem(List<ProcessUpdateItem> processUpdateItems) { List<ProcessUpdateItem> updateItems = processUpdateItems.stream().flatMap(this::handleProcessUpdateItem) .collect(Collectors.toList()); processUpdateItems.addAll(updateItems); } private Stream<ProcessUpdateItem> handleProcessUpdateItem(ProcessUpdateItem processUpdateItem) { List<ProcessUpdateItem> updatedItems = new ArrayList<>(); Metacard metacard = processUpdateItem.getMetacard(); Metacard originalMetacard = processUpdateItem.getOldMetacard(); ProcessResource processResource = processUpdateItem.getProcessResource(); if (shouldProcess(processResource)) { try (TemporaryFileBackedOutputStream fbos = new TemporaryFileBackedOutputStream( DEFAULT_FILE_BACKED_OUTPUT_STREAM_THRESHOLD)) { fbos.write(IOUtils.toByteArray(processResource.getInputStream())); ByteSource byteSource = fbos.asByteSource(); BufferedImage renderedImage = renderImage(byteSource.openStream()); if (renderedImage != null) { addThumbnailToMetacard(metacard, renderedImage); processUpdateItem.markMetacardAsModified(); if (createOverview) { ProcessResource overviewProcessResource = createOverviewResource(renderedImage, metacard); updatedItems.add( new ProcessUpdateItemImpl(overviewProcessResource, metacard, originalMetacard)); } if (storeOriginalImage) { ProcessResource originalImageProcessResource = createOriginalImage( renderImageUsingOriginalDataModel(byteSource.openStream()), metacard); updatedItems.add(new ProcessUpdateItemImpl(originalImageProcessResource, metacard, originalMetacard)); } } } catch (IOException | NitfFormatException | RuntimeException e) { LOGGER.debug(e.getMessage(), e); } catch (InterruptedException e) { LOGGER.error("Rendering failed for {}", processResource.getName(), e); Thread.currentThread().interrupt(); throw new RuntimeException(String.format("Rendering failed for %s", processResource.getName()), e); } } return updatedItems.stream(); } private ProcessResource createOverviewResource(BufferedImage renderedImage, Metacard metacard) { return createDerivedImage(OVERVIEW, renderedImage, metacard, calculateOverviewWidth(renderedImage), calculateOverviewHeight(renderedImage)); } private BufferedImage renderImage(InputStream inputStream) throws NitfFormatException, InterruptedException { return render(inputStream, input -> { try { return input.getRight().render(input.getLeft()); } catch (IOException e) { LOGGER.debug("An error occurred when rendering a nitf", e.getMessage(), e); } return null; }); } private BufferedImage renderImageUsingOriginalDataModel(InputStream inputStream) throws NitfFormatException, InterruptedException { return render(inputStream, input -> { try { return input.getRight().renderToClosestDataModel(input.getLeft()); } catch (IOException e) { LOGGER.debug("An error occurred when rendering a nitf", e.getMessage(), e); } return null; }); } private BufferedImage render(InputStream inputStream, Function<Pair<ImageSegment, NitfRenderer>, BufferedImage> imageSegmentFunction) throws InterruptedException, NitfFormatException { final ThreadLocal<BufferedImage> bufferedImage = new ThreadLocal<>(); if (inputStream != null) { try { available.acquire(); NitfRenderer renderer = nitfRendererSupplier.get(); NitfParserInputFlow parserInputFlow = nitfParserSupplier.get(); parserInputFlow.inputStream(inputStream).allData().forEachImageSegment(segment -> { if (bufferedImage.get() == null) { BufferedImage bi = imageSegmentFunction.apply(new ImmutablePair<>(segment, renderer)); if (bi != null) { bufferedImage.set(bi); } } }).end(); } finally { IOUtils.closeQuietly(inputStream); available.release(); } } BufferedImage image = bufferedImage.get(); bufferedImage.remove(); return image; } private void addThumbnailToMetacard(Metacard metacard, BufferedImage bufferedImage) { try { byte[] thumbnailImage = scaleImage(bufferedImage, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); if (thumbnailImage.length > 0) { metacard.setAttribute(new AttributeImpl(Core.THUMBNAIL, thumbnailImage)); } } catch (IOException e) { LOGGER.debug(e.getMessage(), e); } } private ProcessResource createDerivedImage(String qualifier, BufferedImage image, Metacard metacard, int maxWidth, int maxHeight) { try { byte[] overviewBytes = scaleImage(image, maxWidth, maxHeight); InputStream overviewBytesInputStream = new ByteArrayInputStream(overviewBytes); ProcessResource processResource = new ProcessResourceImpl(metacard.getId(), overviewBytesInputStream, IMAGE_JPEG, buildDerivedImageTitle(metacard.getTitle(), qualifier, JPG), overviewBytes.length, qualifier); ((ProcessResourceImpl) processResource).markAsModified(); addDerivedResourceAttribute(metacard, processResource); return processResource; } catch (IOException e) { LOGGER.debug(e.getMessage(), e); } return null; } private ProcessResource createOriginalImage(BufferedImage image, Metacard metacard) { try { byte[] originalBytes = renderToJpeg2k(image); InputStream originalBytesInputStream = new ByteArrayInputStream(originalBytes); ProcessResource processResource = new ProcessResourceImpl(metacard.getId(), originalBytesInputStream, IMAGE_JPEG2K, buildDerivedImageTitle(metacard.getTitle(), ORIGINAL, JP2), originalBytes.length, ORIGINAL); ((ProcessResourceImpl) processResource).markAsModified(); addDerivedResourceAttribute(metacard, processResource); return processResource; } catch (IOException e) { LOGGER.debug(e.getMessage(), e); } return null; } @VisibleForTesting static String buildDerivedImageTitle(String title, String qualifier, String extension) { String rootFileName = FilenameUtils.getBaseName(title); // title must contain some alphanumeric, human readable characters, or use default filename if (StringUtils.isNotBlank(rootFileName) && StringUtils.isNotBlank(rootFileName.replaceAll("[^A-Za-z0-9]", ""))) { String strippedFilename = rootFileName.replaceAll(INVALID_FILENAME_CHARACTER_REGEX, ""); return String.format(DERIVED_IMAGE_FILENAME_PATTERN, qualifier, strippedFilename, extension) .toLowerCase(); } return String.format("%s.%s", qualifier, JPG).toLowerCase(); } private byte[] scaleImage(final BufferedImage bufferedImage, int width, int height) throws IOException { BufferedImage thumbnail = Thumbnails.of(bufferedImage).size(width, height).outputFormat(JPG) .imageType(BufferedImage.TYPE_3BYTE_BGR).asBufferedImage(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(thumbnail, JPG, outputStream); outputStream.flush(); byte[] thumbnailBytes = outputStream.toByteArray(); outputStream.close(); return thumbnailBytes; } private byte[] renderToJpeg2k(final BufferedImage bufferedImage) throws IOException { BufferedImage imageToCompress = bufferedImage; if (bufferedImage.getColorModel().getNumComponents() == ARGB_COMPONENT_COUNT) { imageToCompress = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR); Graphics2D g = imageToCompress.createGraphics(); g.drawImage(bufferedImage, 0, 0, null); } ByteArrayOutputStream os = new ByteArrayOutputStream(); J2KImageWriter writer = new J2KImageWriter(new J2KImageWriterSpi()); J2KImageWriteParam writeParams = (J2KImageWriteParam) writer.getDefaultWriteParam(); writeParams.setLossless(false); writeParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); writeParams.setCompressionType("JPEG2000"); writeParams.setCompressionQuality(0.0f); ImageOutputStream ios = new MemoryCacheImageOutputStream(os); writer.setOutput(ios); writer.write(null, new IIOImage(imageToCompress, null, null), writeParams); writer.dispose(); ios.close(); return os.toByteArray(); } private void addDerivedResourceAttribute(Metacard metacard, ProcessResource processResource) { Attribute attribute = metacard.getAttribute(Core.DERIVED_RESOURCE_URI); if (attribute == null) { attribute = new AttributeImpl(Core.DERIVED_RESOURCE_URI, processResource.getUri()); } else { AttributeImpl newAttribute = new AttributeImpl(attribute); newAttribute.addValue(processResource.getUri()); attribute = newAttribute; } metacard.setAttribute(attribute); } private int calculateOverviewHeight(BufferedImage image) { final int width = image.getWidth(); final int height = image.getHeight(); if (width >= height) { return (int) Math.round(height * (maxSideLength / width)); } return Math.min(height, (int) maxSideLength); } private int calculateOverviewWidth(BufferedImage image) { final int width = image.getWidth(); final int height = image.getHeight(); if (width >= height) { return Math.min(width, (int) maxSideLength); } return (int) Math.round(width * (maxSideLength / height)); } private boolean shouldProcess(ProcessResource processResource) { if (!isNitfMimeType(processResource.getMimeType())) { LOGGER.debug("Skipping content item (name={}, mimeType={}) because it is not a NITF", processResource.getName(), processResource.getMimeType()); return false; } if (processResource.getSize() / BYTES_PER_MEGABYTE > maxNitfSizeMB) { LOGGER.debug( "Skipping content item (name={}, size={} MB) because it is larger than the configured maximum NITF file size to process of {} MB", processResource.getSize() / BYTES_PER_MEGABYTE, processResource.getName()); return false; } return true; } }