Java tutorial
/* * Copyright 2018, Andrew Lindesay * Distributed under the terms of the MIT License. */ package org.haiku.haikudepotserver.pkg.controller; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; import com.google.common.net.HttpHeaders; import com.google.common.net.MediaType; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.configuration.server.ServerRuntime; import org.haiku.haikudepotserver.dataobjects.Pkg; import org.haiku.haikudepotserver.dataobjects.PkgIcon; import org.haiku.haikudepotserver.job.controller.JobController; import org.haiku.haikudepotserver.job.model.JobService; import org.haiku.haikudepotserver.pkg.RenderedPkgIconRepository; import org.haiku.haikudepotserver.pkg.model.PkgIconExportArchiveJobSpecification; import org.haiku.haikudepotserver.pkg.model.PkgIconService; import org.haiku.haikudepotserver.support.web.AbstractController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.util.Optional; /** * <p>This controller vends the package icon. This may be provided by data stored in the database, or it may be * deferring to the default icon. This controller is also able to take an HTTP PUT request that is able to * update a packages icon. This is not done using JSON-RPC because the binary nature of the data makes transport * of the data in JSON impractical.</p> */ @Controller public class PkgIconController extends AbstractController { protected static Logger LOGGER = LoggerFactory.getLogger(PkgIconController.class); public final static String SEGMENT_PKGICON = "__pkgicon"; @Deprecated private final static String SEGMENT_PKGICON_LEGACY = "pkgicon"; private final static String SEGMENT_GENERICPKGICON = "__genericpkgicon.png"; @Deprecated private final static String SEGMENT_GENERICPKGICON_LEGACY = "genericpkgicon.png"; private final static String SEGMENT_ALL_TAR_BALL = "all.tar.gz"; private final static String KEY_PKGNAME = "pkgname"; private final static String KEY_FORMAT = "format"; public final static String KEY_SIZE = "s"; public final static String KEY_FALLBACK = "f"; private final ServerRuntime serverRuntime; private final PkgIconService pkgIconService; private final JobService jobService; private final RenderedPkgIconRepository renderedPkgIconRepository; private final long startupMillis; public PkgIconController(ServerRuntime serverRuntime, PkgIconService pkgIconService, JobService jobService, RenderedPkgIconRepository renderedPkgIconRepository) { this.serverRuntime = Preconditions.checkNotNull(serverRuntime); this.pkgIconService = Preconditions.checkNotNull(pkgIconService); this.jobService = Preconditions.checkNotNull(jobService); this.renderedPkgIconRepository = Preconditions.checkNotNull(renderedPkgIconRepository); startupMillis = System.currentTimeMillis(); } /** * <p>This method will provide a tar-ball of all of the icons. This can be used by the desktop application. It * honours the typical <code>If-Modified-Since</code> header and will return the "Not Modified" (304) response * if the data that is held by the client is still valid and no new data need to be pulled down. Otherwise it * will return the data to the client; possibly via a re-direct.</p> */ @RequestMapping(value = "/" + SEGMENT_PKGICON + "/" + SEGMENT_ALL_TAR_BALL, method = RequestMethod.GET) public void getAllAsTarBall(HttpServletResponse response, @RequestHeader(value = HttpHeaders.IF_MODIFIED_SINCE, required = false) String ifModifiedSinceHeader) throws IOException { JobController.handleRedirectToJobData(response, jobService, ifModifiedSinceHeader, pkgIconService.getLastPkgIconModifyTimestampSecondAccuracy(serverRuntime.newContext()), new PkgIconExportArchiveJobSpecification()); } @RequestMapping(value = { "/" + SEGMENT_GENERICPKGICON, "/" + SEGMENT_GENERICPKGICON_LEGACY // TODO; remove }, method = RequestMethod.HEAD) public void handleGenericHead(HttpServletResponse response, @RequestParam(value = KEY_SIZE, required = false) Integer size) throws IOException { handleGenericHeadOrGet(RequestMethod.HEAD, response, size, false); } @RequestMapping(value = { "/" + SEGMENT_GENERICPKGICON, "/" + SEGMENT_GENERICPKGICON_LEGACY // TODO; remove }, method = RequestMethod.GET) public void handleGenericGet(HttpServletResponse response, @RequestParam(value = KEY_SIZE, required = false) Integer size) throws IOException { handleGenericHeadOrGet(RequestMethod.GET, response, size, false); } @RequestMapping(value = { "/" + SEGMENT_PKGICON + "/{" + KEY_PKGNAME + "}.{" + KEY_FORMAT + "}", "/" + SEGMENT_PKGICON_LEGACY + "/{" + KEY_PKGNAME + "}.{" + KEY_FORMAT + "}", // TODO; remove }, method = RequestMethod.HEAD) public void handleHeadPkgIcon(HttpServletResponse response, @RequestParam(value = KEY_SIZE, required = false) Integer size, @PathVariable(value = KEY_FORMAT) String format, @PathVariable(value = KEY_PKGNAME) String pkgName, @RequestParam(value = KEY_FALLBACK, required = false) Boolean fallback) throws IOException { handleHeadOrGetPkgIcon(RequestMethod.HEAD, response, size, format, pkgName, fallback); } @RequestMapping(value = { "/" + SEGMENT_PKGICON + "/{" + KEY_PKGNAME + "}.{" + KEY_FORMAT + "}", "/" + SEGMENT_PKGICON_LEGACY + "/{" + KEY_PKGNAME + "}.{" + KEY_FORMAT + "}" // TODO; remove }, method = RequestMethod.GET) public void handleGetPkgIcon(HttpServletResponse response, @RequestParam(value = KEY_SIZE, required = false) Integer size, @PathVariable(value = KEY_FORMAT) String format, @PathVariable(value = KEY_PKGNAME) String pkgName, @RequestParam(value = KEY_FALLBACK, required = false) Boolean fallback) throws IOException { handleHeadOrGetPkgIcon(RequestMethod.GET, response, size, format, pkgName, fallback); } private int normalizeSize(Integer size) { Preconditions.checkArgument(null != size, "the size must be provided"); if (size < RenderedPkgIconRepository.SIZE_MIN) { return RenderedPkgIconRepository.SIZE_MIN; } if (size > RenderedPkgIconRepository.SIZE_MAX) { return RenderedPkgIconRepository.SIZE_MAX; } return size; } /** * @param isAsFallback is true if the request was originally for a package, but fell back to this generic. */ private void handleGenericHeadOrGet(RequestMethod requestMethod, HttpServletResponse response, Integer size, boolean isAsFallback) throws IOException { if (null == size) { size = 64; // largest natural size } size = normalizeSize(size); byte[] data = renderedPkgIconRepository.renderGeneric(size); response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(data.length)); response.setContentType(MediaType.PNG.toString()); if (isAsFallback) { response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); response.setHeader(HttpHeaders.PRAGMA, "no-cache"); response.setHeader(HttpHeaders.EXPIRES, "0"); } else { response.setDateHeader(HttpHeaders.LAST_MODIFIED, startupMillis); } if (requestMethod == RequestMethod.GET) { response.getOutputStream().write(data); } } private void handleHeadOrGetPkgIcon(RequestMethod requestMethod, HttpServletResponse response, Integer size, String format, String pkgName, Boolean fallback) throws IOException { if (null == format) { throw new MissingOrBadFormat(); } if (Strings.isNullOrEmpty(pkgName) || !Pkg.PATTERN_NAME.matcher(pkgName).matches()) { throw new MissingPkgName(); } ObjectContext context = serverRuntime.newContext(); Optional<Pkg> pkg = Pkg.tryGetByName(context, pkgName); // cached if (!pkg.isPresent()) { LOGGER.debug("request for icon for package '{}', but no such package was able to be found", pkgName); throw new PkgNotFound(); } switch (format) { case org.haiku.haikudepotserver.dataobjects.MediaType.EXTENSION_HAIKUVECTORICONFILE: Optional<PkgIcon> hvifPkgIcon = pkg.get().getPkgIcon( org.haiku.haikudepotserver.dataobjects.MediaType.getByExtension(context, format).get(), null); if (hvifPkgIcon.isPresent()) { byte[] data = hvifPkgIcon.get().getPkgIconImage().getData(); response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(data.length)); response.setContentType( org.haiku.haikudepotserver.dataobjects.MediaType.MEDIATYPE_HAIKUVECTORICONFILE); response.setDateHeader(HttpHeaders.LAST_MODIFIED, pkg.get().getModifyTimestampSecondAccuracy().getTime()); if (requestMethod == RequestMethod.GET) { OutputStream outputStream = response.getOutputStream(); outputStream.write(data); outputStream.flush(); } } else { throw new PkgIconNotFound(); } break; case org.haiku.haikudepotserver.dataobjects.MediaType.EXTENSION_PNG: if (null == size) { throw new IllegalArgumentException("the size must be provided when requesting a PNG"); } size = normalizeSize(size); Optional<byte[]> pngImageData = renderedPkgIconRepository.render(size, context, pkg.get()); if (!pngImageData.isPresent()) { if ((null == fallback) || !fallback) { throw new PkgIconNotFound(); } handleGenericHeadOrGet(requestMethod, response, size, true); } else { byte[] data = pngImageData.get(); response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(data.length)); response.setContentType(MediaType.PNG.toString()); response.setDateHeader(HttpHeaders.LAST_MODIFIED, pkg.get().getModifyTimestampSecondAccuracy().getTime()); if (requestMethod == RequestMethod.GET) { OutputStream outputStream = response.getOutputStream(); outputStream.write(data); outputStream.flush(); } } break; default: throw new IllegalStateException("unexpected format; " + format); } } // these are the various errors that can arise in supplying or providing a package icon. @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "the package name must be supplied") private class MissingPkgName extends RuntimeException { } @ResponseStatus(value = HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason = "the format must be supplied and must (presently) be 'png'") private class MissingOrBadFormat extends RuntimeException { } @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "the requested package was unable to found") private class PkgNotFound extends RuntimeException { } @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "the requested package icon was unable to found") private class PkgIconNotFound extends RuntimeException { } }