org.haiku.haikudepotserver.pkg.PkgIconServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.haiku.haikudepotserver.pkg.PkgIconServiceImpl.java

Source

/*
 * Copyright 2018, Andrew Lindesay
 * Distributed under the terms of the MIT License.
 */

package org.haiku.haikudepotserver.pkg;

import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import org.apache.cayenne.DataObject;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.query.EJBQLQuery;
import org.apache.cayenne.query.ObjectSelect;
import org.apache.commons.compress.utils.BoundedInputStream;
import org.haiku.haikudepotserver.dataobjects.MediaType;
import org.haiku.haikudepotserver.dataobjects.Pkg;
import org.haiku.haikudepotserver.dataobjects.PkgIcon;
import org.haiku.haikudepotserver.dataobjects.PkgIconImage;
import org.haiku.haikudepotserver.graphics.ImageHelper;
import org.haiku.haikudepotserver.graphics.bitmap.PngOptimizationService;
import org.haiku.haikudepotserver.pkg.model.BadPkgIconException;
import org.haiku.haikudepotserver.pkg.model.PkgIconConfiguration;
import org.haiku.haikudepotserver.pkg.model.PkgIconService;
import org.haiku.haikudepotserver.pkg.model.PkgService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class PkgIconServiceImpl implements PkgIconService {

    protected static Logger LOGGER = LoggerFactory.getLogger(PkgIconServiceImpl.class);

    @SuppressWarnings("FieldCanBeLocal")
    private static int ICON_SIZE_LIMIT = 100 * 1024; // 100k

    private final RenderedPkgIconRepository renderedPkgIconRepository;
    private final PngOptimizationService pngOptimizationService;
    private final PkgService pkgService;
    private final ImageHelper imageHelper;

    public PkgIconServiceImpl(RenderedPkgIconRepository renderedPkgIconRepository,
            PngOptimizationService pngOptimizationService, PkgService pkgService) {
        this.renderedPkgIconRepository = Preconditions.checkNotNull(renderedPkgIconRepository);
        this.pngOptimizationService = Preconditions.checkNotNull(pngOptimizationService);
        this.pkgService = Preconditions.checkNotNull(pkgService);
        imageHelper = new ImageHelper();
    }

    @Override
    public Date getLastPkgIconModifyTimestampSecondAccuracy(ObjectContext context) {
        Date result = ObjectSelect.query(Pkg.class).where(Pkg.ACTIVE.isTrue()).max(Pkg.ICON_MODIFY_TIMESTAMP)
                .selectOne(context);

        if (null == result) {
            return new Date(0);
        }

        return result;
    }

    @Override
    public void removePkgIcon(ObjectContext context, Pkg pkg) {
        Preconditions.checkArgument(null != context, "the context must be supplied");
        Preconditions.checkArgument(null != pkg, "the package must be supplied");

        context.deleteObjects(deriveDataObjectsToDelete(pkg.getPkgIcons()));
        pkg.setModifyTimestamp();
        pkg.setIconModifyTimestamp(new java.sql.Timestamp(Clock.systemUTC().millis()));

        pkgService.findSubordinatePkgsForMainPkg(context, pkg.getName())
                .forEach(develPkg -> removePkgIcon(context, develPkg));

    }

    @Override
    public PkgIcon storePkgIconImage(InputStream input, MediaType mediaType, Integer expectedSize,
            ObjectContext context, Pkg pkg) throws IOException, BadPkgIconException {

        Preconditions.checkArgument(null != context, "the context is not supplied");
        Preconditions.checkArgument(null != input, "the input must be provided");
        Preconditions.checkArgument(null != mediaType, "the mediaType must be provided");
        Preconditions.checkArgument(null != pkg, "the pkg must be provided");

        byte[] imageData = ByteStreams.toByteArray(new BoundedInputStream(input, ICON_SIZE_LIMIT));

        Optional<PkgIcon> pkgIconOptional;
        Integer size = null;

        switch (mediaType.getCode()) {

        case MediaType.MEDIATYPE_PNG:
            ImageHelper.Size pngSize = imageHelper.derivePngSize(imageData);

            if (null == pngSize) {
                LOGGER.warn(
                        "attempt to set the bitmap (png) package icon for package {}, but the size was invalid; it is not a valid png image",
                        pkg.getName());
                throw new BadPkgIconException("invalid png");
            }

            if (!pngSize.areSides(16) && !pngSize.areSides(32) && !pngSize.areSides(64)) {
                LOGGER.warn(
                        "attempt to set the bitmap (png) package icon for package {}, but the size was invalid; it must be either 32x32 or 16x16 px, but was {}",
                        pkg.getName(), pngSize.toString());
                throw new BadPkgIconException("non-square sizing or unexpected sizing");
            }

            if (null != expectedSize && !pngSize.areSides(expectedSize)) {
                LOGGER.warn(
                        "attempt to set the bitmap (png) package icon for package {}, but the size did not match the expected size",
                        pkg.getName());
                throw new BadPkgIconException("size of image was not as expected");
            }

            try {
                imageData = pngOptimizationService.optimize(imageData);
            } catch (IOException ioe) {
                throw new RuntimeException("the png optimization process has failed; ", ioe);
            }

            size = pngSize.width;
            pkgIconOptional = pkg.getPkgIcon(mediaType, pngSize.width);
            break;

        case MediaType.MEDIATYPE_HAIKUVECTORICONFILE:
            if (!imageHelper.looksLikeHaikuVectorIconFormat(imageData)) {
                LOGGER.warn(
                        "attempt to set the vector (hvif) package icon for package {}, but the data does not look like hvif",
                        pkg.getName());
                throw new BadPkgIconException();
            }
            pkgIconOptional = pkg.getPkgIcon(mediaType, null);
            break;

        default:
            throw new IllegalStateException("unhandled media type; " + mediaType.getCode());

        }

        PkgIconImage pkgIconImage;

        if (pkgIconOptional.isPresent()) {
            pkgIconImage = pkgIconOptional.get().getPkgIconImage();
        } else {
            PkgIcon pkgIcon = context.newObject(PkgIcon.class);
            pkg.addToManyTarget(Pkg.PKG_ICONS.getName(), pkgIcon, true);
            pkgIcon.setMediaType(mediaType);
            pkgIcon.setSize(size);
            pkgIconImage = context.newObject(PkgIconImage.class);
            pkgIcon.addToManyTarget(PkgIcon.PKG_ICON_IMAGES.getName(), pkgIconImage, true);
            pkgIconOptional = Optional.of(pkgIcon);
        }

        pkgIconImage.setData(imageData);
        pkg.setModifyTimestamp();
        pkg.setIconModifyTimestamp(new java.sql.Timestamp(Clock.systemUTC().millis()));
        renderedPkgIconRepository.evict(context, pkg);

        if (null != size) {
            LOGGER.info("the icon {}px for package {} has been updated", size, pkg.getName());
        } else {
            LOGGER.info("the icon for package {} has been updated", pkg.getName());
        }

        PkgIcon pkgIcon = pkgIconOptional.orElseThrow(IllegalStateException::new);

        for (Pkg subordinatePkg : pkgService.findSubordinatePkgsForMainPkg(context, pkg.getName())) {
            replicatePkgIcon(context, pkgIcon, subordinatePkg);
        }

        return pkgIcon;
    }

    private List<MediaType> getInUsePkgIconMediaTypes(final ObjectContext context) {
        EJBQLQuery query = new EJBQLQuery(String.join(" ", "SELECT",
                "DISTINCT pi." + PkgIcon.MEDIA_TYPE.getName() + "." + MediaType.CODE.getName(), "FROM",
                PkgIcon.class.getSimpleName(), "pi"));

        final List<String> codes = (List<String>) context.performQuery(query);

        return codes.stream().map(c -> MediaType.tryGetByCode(context, c).get()).collect(Collectors.toList());

    }

    private PkgIcon replicatePkgIcon(ObjectContext context, PkgIcon pkgIcon, Pkg targetPkg)
            throws IOException, BadPkgIconException {
        return storePkgIconImage(new ByteArrayInputStream(pkgIcon.getPkgIconImage().getData()),
                pkgIcon.getMediaType(), pkgIcon.getSize(), context, targetPkg);
    }

    @Override
    public void replicatePkgIcons(ObjectContext context, Pkg sourcePkg, Pkg targetPkg)
            throws IOException, BadPkgIconException {

        Preconditions.checkArgument(null != context, "the context must be supplied");
        Preconditions.checkArgument(null != sourcePkg, "the source pkg must be supplied");
        Preconditions.checkArgument(null != targetPkg, "the target pkg must be supplied");

        // first remove all of the icons from the target pkg that do not exist in the source.

        List<DataObject> targetPkgIconsDataObjectsToDelete = deriveDataObjectsToDelete(targetPkg
                .getPkgIcons().stream().filter(
                        (tpi) -> sourcePkg.getPkgIcons().stream()
                                .noneMatch((spi) -> Objects.equals(spi.getMediaType(), tpi.getMediaType())
                                        && Objects.equals(spi.getSize(), tpi.getSize())))
                .collect(Collectors.toList()));

        context.deleteObjects(targetPkgIconsDataObjectsToDelete);

        // now merge in those from the source.

        for (PkgIcon pkgIcon : sourcePkg.getPkgIcons()) {
            replicatePkgIcon(context, pkgIcon, targetPkg);
        }
    }

    private List<Integer> getInUsePkgIconSizes(ObjectContext context, MediaType mediaType) {
        EJBQLQuery query = new EJBQLQuery(String.join(" ", "SELECT", "DISTINCT pi." + PkgIcon.SIZE.getName(),
                "FROM", PkgIcon.class.getSimpleName(), "pi WHERE pi." + PkgIcon.MEDIA_TYPE.getName(), "=",
                ":mediaType"));

        query.setParameter("mediaType", mediaType);

        return (List<Integer>) context.performQuery(query);
    }

    @Override
    public List<PkgIconConfiguration> getInUsePkgIconConfigurations(ObjectContext objectContext) {

        Preconditions.checkArgument(null != objectContext, "the object context must be supplied");

        List<PkgIconConfiguration> result = new ArrayList<>();

        for (MediaType mediaType : getInUsePkgIconMediaTypes(objectContext)) {
            List<Integer> sizes = getInUsePkgIconSizes(objectContext, mediaType);

            if (sizes.isEmpty()) {
                result.add(new PkgIconConfiguration(mediaType, null));
            } else {
                for (Integer size : sizes) {
                    result.add(new PkgIconConfiguration(mediaType, size));
                }
            }
        }

        Collections.sort(result);

        return result;
    }

    private List<DataObject> deriveDataObjectsToDelete(List<PkgIcon> pkgIcons) {
        return pkgIcons.stream().flatMap((pi) -> Arrays.stream(new DataObject[] { pi.getPkgIconImage(), pi }))
                .collect(Collectors.toList());
    }

}