[haiku-depot-web] [haiku-depot-web-app] 3 new revisions pushed by haiku.li...@xxxxxxxxx on 2014-02-24 10:23 GMT

  • From: haiku-depot-web-app@xxxxxxxxxxxxxx
  • To: haiku-depot-web@xxxxxxxxxxxxx
  • Date: Mon, 24 Feb 2014 10:24:25 +0000

master moved from f2295f14696f to b8dc530bc215

3 new revisions:

Revision: 827b994410c3
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Mon Feb 24 08:20:27 2014 UTC
Log:      + handling for upload of hvif files for pkg icons
http://code.google.com/p/haiku-depot-web-app/source/detail?r=827b994410c3

Revision: d14c61e65059
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Mon Feb 24 10:10:13 2014 UTC
Log:      + icon and screenshot handling changes
http://code.google.com/p/haiku-depot-web-app/source/detail?r=d14c61e65059

Revision: b8dc530bc215
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Mon Feb 24 10:23:41 2014 UTC
Log:      + documentation updates
http://code.google.com/p/haiku-depot-web-app/source/detail?r=b8dc530bc215

==============================================================================
Revision: 827b994410c3
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Mon Feb 24 08:20:27 2014 UTC
Log:      + handling for upload of hvif files for pkg icons

http://code.google.com/p/haiku-depot-web-app/source/detail?r=827b994410c3

Added:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/ConfigurePkgIconRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/ConfigurePkgIconResult.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/BadPkgIconException.java /haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.3__MediaType_HVIF.sql
 /haikudepotserver-webapp/src/test/resources/sample.hvif
Modified:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/RemovePkgIconRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/Constants.java
 /haikudepotserver-docs/src/main/latex/docs/part-api.tex
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ErrorResolverImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/MediaType.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/Pkg.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgIcon.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/PkgService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgIconController.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/ImageHelper.java
 /haikudepotserver-webapp/src/main/resources/messages.properties
 /haikudepotserver-webapp/src/main/resources/spring/servlet-context.xml
 /haikudepotserver-webapp/src/main/webapp/js/app/constants.js
 /haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgicon.html
/haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgiconcontroller.js /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js /haikudepotserver-webapp/src/main/webapp/js/app/directive/filesupplydirective.js
 /haikudepotserver-webapp/src/main/webapp/js/app/service/jsonrpcservice.js
 /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgiconservice.js
/haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/PkgApiIT.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/support/IntegrationTestSupportService.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/pkg/controller/PkgIconControllerIT.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/support/ImageHelperTest.java

=======================================
--- /dev/null
+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/ConfigurePkgIconRequest.java Mon Feb 24 08:20:27 2014 UTC
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.api1.model.pkg;
+
+import java.util.List;
+
+public class ConfigurePkgIconRequest {
+
+    public String pkgName;
+
+    public List<PkgIcon> pkgIcons;
+
+    public static class PkgIcon {
+
+        public String mediaTypeCode;
+        public Integer size;
+        public String dataBase64;
+
+        public PkgIcon() {
+        }
+
+ public PkgIcon(String mediaTypeCode, Integer size, String dataBase64) {
+            this.mediaTypeCode = mediaTypeCode;
+            this.size = size;
+            this.dataBase64 = dataBase64;
+        }
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/ConfigurePkgIconResult.java Mon Feb 24 08:20:27 2014 UTC
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.api1.model.pkg;
+
+public class ConfigurePkgIconResult {
+}
=======================================
--- /dev/null
+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/BadPkgIconException.java Mon Feb 24 08:20:27 2014 UTC
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.api1.support;
+
+/**
+ * <p>This exception is thrown when there is some issue with the format of the icon data or the size of it.</p>
+ */
+
+public class BadPkgIconException extends Exception {
+
+    private String mediaTypeCode;
+    private Integer size;
+
+    public BadPkgIconException(String mediaTypeCode, Integer size) {
+        this.mediaTypeCode = mediaTypeCode;
+        this.size = size;
+    }
+
+ public BadPkgIconException(String mediaTypeCode, Integer size, Throwable cause) {
+        super(cause);
+        this.mediaTypeCode = mediaTypeCode;
+        this.size = size;
+    }
+
+    public String getMediaTypeCode() {
+        return mediaTypeCode;
+    }
+
+    public Integer getSize() {
+        return size;
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.3__MediaType_HVIF.sql Mon Feb 24 08:20:27 2014 UTC
@@ -0,0 +1,1 @@
+INSERT INTO media_type (id,code) VALUES ((SELECT nextval('haikudepot.media_type_seq')),'application/x-vnd.haiku-icon');
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/test/resources/sample.hvif Mon Feb 24 08:20:27 2014 UTC
@@ -0,0 +1,2 @@
+ncifÿª8-B%.5,@)=/C8D6?:I3P-K9UWJTSZAP>QBO:Åý¹3Çs¼7L(
+
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java Tue Feb 18 09:54:04 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java Mon Feb 24 08:20:27 2014 UTC
@@ -7,6 +7,7 @@

 import com.googlecode.jsonrpc4j.JsonRpcService;
 import org.haikuos.haikudepotserver.api1.model.pkg.*;
+import org.haikuos.haikudepotserver.api1.support.BadPkgIconException;
 import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException;

 /**
@@ -30,6 +31,13 @@

GetPkgResult getPkg(GetPkgRequest request) throws ObjectNotFoundException;

+    /**
+ * <p>This request will configure the icons for the package nominated. Note that only certain configurations of + * icon data may be acceptable; for example, it will require a 16x16px and 32x32px bitmap image.</p>
+     */
+
+ ConfigurePkgIconResult configurePkgIcon(ConfigurePkgIconRequest request) throws ObjectNotFoundException, BadPkgIconException;
+
     /**
      * <p>This request will remove any icons from the package.</p>
      */
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/RemovePkgIconRequest.java Wed Feb 12 10:07:08 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/RemovePkgIconRequest.java Mon Feb 24 08:20:27 2014 UTC
@@ -11,18 +11,17 @@
* <p>This is the name of the package that you wish to reset the icon for.</p>
      */

-    public String name;
+    public String pkgName;

     public RemovePkgIconRequest() {
     }

+    public RemovePkgIconRequest(String pkgName) {

-    public RemovePkgIconRequest(String name) {
-
-        if(null==name || 0==name.length()) {
- throw new IllegalArgumentException("the name must be supplied when removing the icon for a package");
+        if(null==pkgName || 0==pkgName.length()) {
+ throw new IllegalArgumentException("the package name must be supplied when removing the icon for a package");
         }

-        this.name = name;
+        this.pkgName = pkgName;
     }
 }
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/Constants.java Wed Nov 20 10:28:55 2013 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/Constants.java Mon Feb 24 08:20:27 2014 UTC
@@ -11,5 +11,6 @@
     public final static int ERROR_CODE_OBJECTNOTFOUND = -32801;
     public final static int ERROR_CODE_CAPTCHABADRESPONSE = -32802;
     public final static int ERROR_CODE_AUTHORIZATIONFAILURE = -32803;
+    public final static int ERROR_CODE_BADPKGICON = -32804;

 }
=======================================
--- /haikudepotserver-docs/src/main/latex/docs/part-api.tex Tue Feb 18 10:09:54 2014 UTC +++ /haikudepotserver-docs/src/main/latex/docs/part-api.tex Mon Feb 24 08:20:27 2014 UTC
@@ -154,46 +154,30 @@

 \subsubsection{Get Package Icon}

-This API is able to provide the icon for a package. If there is no icon stored then this method will provide a fall-back image. The request will return a {\tt Last-Modified} header and will process a {\tt If-Modified-Since} header on the request if there is one present. The timestamps here will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. +This API is able to provide the icon for a package. If there is no icon stored then this method will provide a fall-back image if the ``f'' query parameter is configured to ``true'' --- otherwise it will return a 404 HTTP status code. Providing a fallback image may not be possible in all cases. The request will return a {\tt Last-Modified} header. The timestamps here will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. The path includes a {\it mediatype-extension} which can have one of the following values;

 \begin{itemize}
-\item HTTP Method : GET, HEAD
-\item Path : /pkgicon/$<$pkgname$>$.png
-\item Response Content-Type : image/png
-\item Query Parameters
-  \begin{itemize}
- \item {\bf size} : Either 16 or 32 for the number of pixels; other values are not allowed
-  \end{itemize}
-\item Expected HTTP Status Codes
-  \begin{itemize}
-  \item {\bf 200} : The icon is provided in the response (for GET)
-  \item {\bf 415} : The path did not include ".png" or the size is invalid
-  \item {\bf 400} : The package name was not supplied
-  \item {\bf 404} : The package was not found
-  \end{itemize}
+\item png
+\item hvif
 \end{itemize}

-An example URL is;
-
-\framebox{\tt http://localhost:8080/pkgicon/apr.png?size=32}
-
-\subsubsection{Put Package Icon}
-
-This API is able to store an icon for a given size for a package.
+For example ``somepage.png''.

 \begin{itemize}
-\item HTTP Method : PUT
-\item Path : /pkgicon/$<$pkgname$>$.png
+\item HTTP Method : GET, HEAD
+\item Path : /pkgicon/$<$pkgname$>$.$<$mediatype-extension$>$
+\item Response Content-Type : ``image/png'' or ``application/x-vnd.haiku-icon''
 \item Query Parameters
   \begin{itemize}
- \item {\bf size} : Either 16 or 32 for the number of pixels; other values are not allowed + \item {\bf s} : Either 16 or 32 for the number of pixels; omitt for hivf files + \item {\bf f} : ``true'' will yield a fallback image in the response if possible
   \end{itemize}
 \item Expected HTTP Status Codes
   \begin{itemize}
-  \item {\bf 200} : The icon was stored
- \item {\bf 415} : The path did not include ".png" or the size is invalid or the supplied data is not in PNG format or the size of the suppied data does not agree with the size specified in the query parameter. - \item {\bf 404} : The package identified in the path was not able to be found
+  \item {\bf 200} : The icon is provided in the response (for GET)
+  \item {\bf 415} : The path did not include ".png" or the size is invalid
   \item {\bf 400} : The package name was not supplied
+  \item {\bf 404} : The package was not found or no image was present
   \end{itemize}
 \end{itemize}

=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Tue Feb 18 09:54:04 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Mon Feb 24 08:20:27 2014 UTC
@@ -5,31 +5,35 @@

 package org.haikuos.haikudepotserver.api1;

-import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
+import com.google.common.base.*;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.common.net.*;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.configuration.server.ServerRuntime;
 import org.haikuos.haikudepotserver.api1.model.pkg.*;
import org.haikuos.haikudepotserver.api1.support.AuthorizationFailureException;
+import org.haikuos.haikudepotserver.api1.support.BadPkgIconException;
 import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException;
+import org.haikuos.haikudepotserver.dataobjects.MediaType;
 import org.haikuos.haikudepotserver.security.model.Permission;
 import org.haikuos.haikudepotserver.dataobjects.*;
 import org.haikuos.haikudepotserver.pkg.PkgService;
 import org.haikuos.haikudepotserver.pkg.model.PkgSearchSpecification;
 import org.haikuos.haikudepotserver.security.AuthorizationService;
+import com.googlecode.jsonrpc4j.Base64;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;

 import javax.annotation.Resource;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;

 /**
  * <p>See {@link PkgApi} for details on the methods this API affords.</p>
@@ -225,7 +229,7 @@
                         context,
                         pkgOptional.get(),
                         Lists.newArrayList(
-                            architectureOptional.get(),
+                                architectureOptional.get(),
Architecture.getByCode(context, Architecture.CODE_ANY).get(), Architecture.getByCode(context, Architecture.CODE_SOURCE).get()));

@@ -245,18 +249,141 @@

         return result;
     }
+
+    private boolean contains(
+            final List<ConfigurePkgIconRequest.PkgIcon> pkgIconApis,
+            final String mediaTypeCode,
+            final Integer size) {
+
+        Preconditions.checkNotNull(pkgIconApis);
+        Preconditions.checkState(!Strings.isNullOrEmpty(mediaTypeCode));
+        Preconditions.checkNotNull(size);
+
+ return Iterables.tryFind(pkgIconApis, new Predicate<ConfigurePkgIconRequest.PkgIcon>() {
+            @Override
+            public boolean apply(ConfigurePkgIconRequest.PkgIcon input) {
+ return input.mediaTypeCode.equals(mediaTypeCode) && (null!=input.size) && (input.size == size);
+            }
+        }).isPresent();
+
+    }
+
+    @Override
+ public ConfigurePkgIconResult configurePkgIcon(ConfigurePkgIconRequest request) throws ObjectNotFoundException, BadPkgIconException {
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!Strings.isNullOrEmpty(request.pkgName));
+
+        final ObjectContext context = serverRuntime.getContext();
+ Optional<Pkg> pkgOptional = Pkg.getByName(context, request.pkgName);
+
+        if(!pkgOptional.isPresent()) {
+ throw new ObjectNotFoundException(Pkg.class.getSimpleName(), request.pkgName);
+        }
+
+        User user = obtainAuthenticatedUser(context);
+
+ if(!authorizationService.check(context, user, pkgOptional.get(), Permission.PKG_EDITICON)) { + logger.warn("attempt to configure the icon for package {}, but the user {} is not able to", pkgOptional.get().getName(), user.getNickname());
+            throw new AuthorizationFailureException();
+        }
+
+        // insert or override the icons
+
+        int updated = 0;
+        int removed = 0;
+
+        Set<PkgIcon> createdOrUpdatedPkgIcons = Sets.newHashSet();
+
+        if(null!=request.pkgIcons && !request.pkgIcons.isEmpty()) {
+
+            if(
+ !contains(request.pkgIcons, com.google.common.net.MediaType.PNG.toString(), 16) + || !contains(request.pkgIcons, com.google.common.net.MediaType.PNG.toString(), 32)) { + throw new IllegalStateException("pkg icons must contain a 16x16px and 32x32px png icon variant");
+            }
+
+ for(ConfigurePkgIconRequest.PkgIcon pkgIconApi : request.pkgIcons) {
+
+ Optional<MediaType> mediaTypeOptional = MediaType.getByCode(context, pkgIconApi.mediaTypeCode);
+
+                if(!mediaTypeOptional.isPresent()) {
+ throw new IllegalStateException("unknown media type; "+pkgIconApi.mediaTypeCode);
+                }
+
+                if(Strings.isNullOrEmpty(pkgIconApi.dataBase64)) {
+ throw new IllegalStateException("the base64 data must be supplied with the request to configure a pkg icon");
+                }
+
+                if(Strings.isNullOrEmpty(pkgIconApi.mediaTypeCode)) {
+ throw new IllegalStateException("the mediaTypeCode must be supplied to configure a pkg icon");
+                }
+
+                try {
+                    byte[] data = Base64.decode(pkgIconApi.dataBase64);
+ ByteArrayInputStream dataInputStream = new ByteArrayInputStream(data);
+
+                    createdOrUpdatedPkgIcons.add(
+                            pkgService.storePkgIconImage(
+                                    dataInputStream,
+                                    mediaTypeOptional.get(),
+                                    pkgIconApi.size,
+                                    context,
+                                    pkgOptional.get()
+                            )
+                    );
+
+                    updated++;
+                }
+                catch(IOException ioe) {
+ throw new RuntimeException("a problem has arisen storing the data for an icon",ioe);
+                }
+ catch(org.haikuos.haikudepotserver.pkg.model.BadPkgIconException bpie) { + throw new BadPkgIconException(pkgIconApi.mediaTypeCode, pkgIconApi.size, bpie);
+                }
+
+            }
+
+        }
+
+ // now we have some icons stored which may not be in the replacement data; we should remove those ones.
+
+ for(PkgIcon pkgIcon : ImmutableList.copyOf(pkgOptional.get().getPkgIcons())) {
+            if(!createdOrUpdatedPkgIcons.contains(pkgIcon)) {
+                context.deleteObjects(
+                        pkgIcon.getPkgIconImage().get(),
+                        pkgIcon);
+
+                removed++;
+            }
+        }
+
+        pkgOptional.get().setModifyTimestamp();
+
+        context.commitChanges();
+
+        logger.info(
+                "did configure icons for pkg {} (updated {}, removed {})",
+                new Object[] {
+                        pkgOptional.get().getName(),
+                        updated,
+                        removed
+                }
+        );
+
+        return new ConfigurePkgIconResult();
+    }

     @Override
public RemovePkgIconResult removePkgIcon(RemovePkgIconRequest request) throws ObjectNotFoundException {

         Preconditions.checkNotNull(request);
-        Preconditions.checkState(!Strings.isNullOrEmpty(request.name));
+        Preconditions.checkState(!Strings.isNullOrEmpty(request.pkgName));

         final ObjectContext context = serverRuntime.getContext();
-        Optional<Pkg> pkgOptional = Pkg.getByName(context, request.name);
+ Optional<Pkg> pkgOptional = Pkg.getByName(context, request.pkgName);

         if(!pkgOptional.isPresent()) {
- throw new ObjectNotFoundException(Pkg.class.getSimpleName(), request.name); + throw new ObjectNotFoundException(Pkg.class.getSimpleName(), request.pkgName);
         }

         User user = obtainAuthenticatedUser(context);
@@ -267,9 +394,9 @@
         }

for(PkgIcon pkgIcon : ImmutableList.copyOf(pkgOptional.get().getPkgIcons())) {
-           context.deleteObjects(
-                   pkgIcon.getPkgIconImage().get(),
-                   pkgIcon);
+            context.deleteObjects(
+                    pkgIcon.getPkgIconImage().get(),
+                    pkgIcon);
         }

         pkgOptional.get().setModifyTimestamp();
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ErrorResolverImpl.java Sun Feb 9 10:00:28 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ErrorResolverImpl.java Mon Feb 24 08:20:27 2014 UTC
@@ -9,6 +9,7 @@
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.googlecode.jsonrpc4j.DefaultErrorResolver;
 import com.googlecode.jsonrpc4j.ErrorResolver;
 import org.apache.cayenne.validation.BeanValidationFailure;
@@ -45,6 +46,22 @@
                     "captchabadresponse",
                     null);
         }
+
+        if(BadPkgIconException.class.isAssignableFrom(t.getClass())) {
+ BadPkgIconException badPkgIconException = (BadPkgIconException) t;
+
+            Map<String,Object> errorData = Maps.newHashMap();
+ errorData.put("mediaTypeCode", badPkgIconException.getMediaTypeCode());
+
+            if(null!=badPkgIconException.getSize()) {
+                errorData.put("size", badPkgIconException.getSize());
+            }
+
+            return new JsonError(
+                    Constants.ERROR_CODE_BADPKGICON,
+                    "badpkgicon",
+                    errorData);
+        }

         // special output for the object not found exceptions

@@ -55,7 +72,7 @@
                     Constants.ERROR_CODE_OBJECTNOTFOUND,
                     "objectnotfound",
                     ImmutableMap.of(
- "entityname", objectNotFoundException.getEntityName(), + "entityName", objectNotFoundException.getEntityName(), "identifier", objectNotFoundException.getIdentifier()
                     )
             );
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/MediaType.java Wed Dec 11 08:25:33 2013 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/MediaType.java Mon Feb 24 08:20:27 2014 UTC
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013, Andrew Lindesay
+ * Copyright 2013-2014, Andrew Lindesay
  * Distributed under the terms of the MIT License.
  */

@@ -17,6 +17,32 @@
 import java.util.List;

 public class MediaType extends _MediaType {
+
+ public final static String MEDIATYPE_HAIKUVECTORICONFILE = "application/x-vnd.haiku-icon";
+
+    public final static String EXTENSION_HAIKUVECTORICONFILE = "hvif";
+
+    public final static String EXTENSION_PNG = "png";
+
+    /**
+ * <p>Files can have extensions that help to signify what sort of files they are. For example, a PNG file would + * have the extension "png". This method will be able to return a media type for a given file extension.</p>
+     */
+
+ public static Optional<MediaType> getByExtension(ObjectContext context, String extension) {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkState(!Strings.isNullOrEmpty(extension));
+
+        if(extension.equals(EXTENSION_HAIKUVECTORICONFILE)) {
+            return getByCode(context, MEDIATYPE_HAIKUVECTORICONFILE);
+        }
+
+        if(extension.equals(EXTENSION_PNG)) {
+ return getByCode(context, com.google.common.net.MediaType.PNG.toString());
+        }
+
+        return Optional.absent();
+    }

public static Optional<MediaType> getByCode(ObjectContext context, String code) {
         Preconditions.checkNotNull(context);
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/Pkg.java Wed Feb 12 10:07:08 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/Pkg.java Mon Feb 24 08:20:27 2014 UTC
@@ -7,8 +7,10 @@

 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.exp.ExpressionFactory;
@@ -48,37 +50,35 @@

     }

- public Optional<PkgIconImage> getPkgIconImage(final MediaType mediaType, final int size) { + public Optional<PkgIcon> getPkgIcon(final MediaType mediaType, final Integer size) {
         Preconditions.checkNotNull(mediaType);
-        Preconditions.checkState(16==size||32==size);

-        Optional<PkgIcon> pkgIconOptional = getPkgIcon(mediaType,size);
+        List<PkgIcon> icons = getPkgIcons(mediaType,size);

-        if(pkgIconOptional.isPresent()) {
- Optional<PkgIconImage> pkgIconImageOptional = pkgIconOptional.get().getPkgIconImage();
+        switch(icons.size()) {
+            case 0:
+                return Optional.absent();

-            if(pkgIconImageOptional.isPresent()) {
-                return pkgIconImageOptional;
-            }
-            else {
- throw new IllegalStateException("a pkg icon does not have an image associated with it.");
-            }
-        }
+            case 1:
+                return Optional.of(icons.get(0));

-        return Optional.absent();
+            default:
+ throw new IllegalStateException("more than one pkg icon of media type "+mediaType.getCode()+" of size "+size+" on pkg "+getName());
+        }
     }

- public Optional<PkgIcon> getPkgIcon(final MediaType mediaType, final int size) { + private List<PkgIcon> getPkgIcons(final MediaType mediaType, final Integer size) {
         Preconditions.checkNotNull(mediaType);
-        Preconditions.checkState(16==size||32==size);

-        for(PkgIcon pkgIcon : getPkgIcons()) {
- if(pkgIcon.getMediaType().equals(mediaType) && pkgIcon.getSize()==size) {
-                return Optional.of(pkgIcon);
+ return Lists.newArrayList(Iterables.filter(getPkgIcons(), new Predicate<PkgIcon>() {
+            @Override
+            public boolean apply(PkgIcon o) {
+                return
+                        o.getMediaType().equals(mediaType)
+                                && (null==size) == (null==o.getSize())
+ && ((null==size) || size.equals(o.getSize()));
             }
-        }
-
-        return Optional.absent();
+        }));
     }

     public void setModifyTimestamp() {
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgIcon.java Wed Dec 11 08:25:33 2013 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgIcon.java Mon Feb 24 08:20:27 2014 UTC
@@ -1,17 +1,24 @@
 /*
- * Copyright 2013, Andrew Lindesay
+ * Copyright 2013-2014, Andrew Lindesay
  * Distributed under the terms of the MIT License.
  */

 package org.haikuos.haikudepotserver.dataobjects;

 import com.google.common.base.Optional;
+import com.google.common.net.*;
+import com.google.common.net.MediaType;
+import org.apache.cayenne.validation.BeanValidationFailure;
+import org.apache.cayenne.validation.ValidationResult;
 import org.haikuos.haikudepotserver.dataobjects.auto._PkgIcon;

 import java.util.List;

 public class PkgIcon extends _PkgIcon {

+ public final static String VALIDATION_REQUIREDFORBITMAP = "requiredforbitmap"; + public final static String VALIDATION_NOTALLOWEDFORVECTOR = "notallowedforvector";
+
     /**
* <p>As there should be only one of these, if there are two then this method will throw an
      * {@link IllegalStateException}.</p>
@@ -27,5 +34,25 @@
throw new IllegalStateException("more than one pkg icon image found on an icon image");
         }
     }
+
+    @Override
+    protected void validateForSave(ValidationResult validationResult) {
+        super.validateForSave(validationResult);
+
+ // vector artwork should not be stored with a size because it makes no sense.
+
+ if(com.google.common.net.MediaType.PNG.equals(getMediaType().getCode())) {
+            if(null==getSize()) {
+ validationResult.addFailure(new BeanValidationFailure(this,SIZE_PROPERTY,VALIDATION_REQUIREDFORBITMAP));
+            }
+        }
+
+ if(org.haikuos.haikudepotserver.dataobjects.MediaType.MEDIATYPE_HAIKUVECTORICONFILE.equals(getMediaType().getCode())) {
+            if(null!=getSize()) {
+ validationResult.addFailure(new BeanValidationFailure(this,SIZE_PROPERTY,VALIDATION_NOTALLOWEDFORVECTOR));
+            }
+        }
+
+    }

 }
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/PkgService.java Tue Feb 18 09:54:04 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/PkgService.java Mon Feb 24 08:20:27 2014 UTC
@@ -11,6 +11,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
+import com.google.common.net.*;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.exp.Expression;
@@ -18,6 +19,7 @@
 import org.apache.cayenne.query.EJBQLQuery;
 import org.apache.cayenne.query.SelectQuery;
 import org.haikuos.haikudepotserver.dataobjects.*;
+import org.haikuos.haikudepotserver.dataobjects.MediaType;
 import org.haikuos.haikudepotserver.dataobjects.Pkg;
 import org.haikuos.haikudepotserver.dataobjects.PkgUrlType;
 import org.haikuos.haikudepotserver.pkg.model.BadPkgIconException;
@@ -210,7 +212,11 @@
     // ------------------------------
     // ICONS

-    private void writeGenericIconImage(
+    /**
+     * <p>This will output a bitmap image for a generic icon.</p>
+     */
+
+    public void writeGenericIconImage(
             OutputStream output,
             int size) throws IOException {

@@ -236,91 +242,89 @@
     }

     /**
- * <p>This method will write the package's icon to the supplied output stream. If there is no icon stored
-     * for the package then a generic icon will be provided instead.</p>
+ * <p>This method will write the icon data supplied in the input to the package as its icon. Note that the icon + * must comply with necessary characteristics; for example it must be either 16 or 32 pixels along both its sides
+     * if it is a PNG.  If it is non-compliant then an instance of
+ * {@link org.haikuos.haikudepotserver.pkg.model.BadPkgIconException} will be thrown.</p>
      */

-    public void writePkgIconImage(
-            OutputStream output,
-            ObjectContext context,
-            Pkg pkg,
-            int size) throws IOException {
-
-        Preconditions.checkNotNull(output);
-        Preconditions.checkNotNull(context);
-        Preconditions.checkNotNull(pkg);
-        Preconditions.checkState(16==size||32==size);
-
-        Optional<PkgIconImage> pkgIconImageOptional = pkg.getPkgIconImage(
- MediaType.getByCode(context, com.google.common.net.MediaType.PNG.toString()).get(),
-                size);
-
-        if(pkgIconImageOptional.isPresent()) {
-            output.write(pkgIconImageOptional.get().getData());
-        }
-        else {
-            writeGenericIconImage(output, size);
-        }
-    }
-
-    /**
- * <p>This method will write the PNG data supplied in the input to the package as its icon. Note that the icon - * must comply with necessary characteristics; for example it must be either 16 or 32 pixels along both its sides. - * If it is non-compliant then an instance of {@link org.haikuos.haikudepotserver.pkg.model.BadPkgIconException} will be thrown.</p>
-     */
-
-    public void storePkgIconImage(
+    public PkgIcon storePkgIconImage(
             InputStream input,
-            int expectedSize,
+            MediaType mediaType,
+            Integer expectedSize,
             ObjectContext context,
             Pkg pkg) throws IOException, BadPkgIconException {

         Preconditions.checkNotNull(input);
+        Preconditions.checkNotNull(mediaType);
         Preconditions.checkNotNull(context);
         Preconditions.checkNotNull(pkg);
-        Preconditions.checkState(16==expectedSize||32==expectedSize);

-        byte[] pngData = toByteArray(input, ICON_SIZE_LIMIT);
-        ImageHelper.Size size =  imageHelper.derivePngSize(pngData);
+        byte[] imageData = toByteArray(input, ICON_SIZE_LIMIT);
+        Optional<PkgIcon> pkgIconOptional = null;
+        Integer size = null;

-        if(null==size) {
- logger.warn("attempt to set the package icon for package {}, but the data does not look like png",pkg.getName());
-            throw new BadPkgIconException();
-        }
+ if(com.google.common.net.MediaType.PNG.toString().equals(mediaType.getCode())) {

- // check that the file roughly looks like PNG and that the size can be
-        // parsed and that the size fits the requirements for the icon.
+ 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();
+            }
+
+            if(!pngSize.areSides(16) && !pngSize.areSides(32)) {
+ 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();
+            }
+
+            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();
+            }

-        if(null==size || (!size.areSides(16) && !size.areSides(32))) {
- logger.warn("attempt to set the package icon for package {}, but the size was not able to be established; either it is not a valid png image or the size of the png image is not appropriate",pkg.getName());
-            throw new BadPkgIconException();
+            size = pngSize.width;
+            pkgIconOptional = pkg.getPkgIcon(mediaType, pngSize.width);
         }
-
-        if(expectedSize != size.height && expectedSize != size.width) {
- logger.warn("attempt to set the package icon for package {}, but the size was note the expected {}px",pkg.getName(),expectedSize);
-            throw new BadPkgIconException();
+        else {
+ if(MediaType.MEDIATYPE_HAIKUVECTORICONFILE.equals(mediaType.getCode())) { + 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);
+            }
+            else {
+ throw new IllegalStateException("unhandled media type; "+mediaType.getCode());
+            }
         }

- MediaType png = MediaType.getByCode(context, com.google.common.net.MediaType.PNG.toString()).get(); - Optional<PkgIconImage> pkgIconImageOptional = pkg.getPkgIconImage(png,size.width);
         PkgIconImage pkgIconImage = null;

-        if(pkgIconImageOptional.isPresent()) {
-            pkgIconImage = pkgIconImageOptional.get();
+        if(pkgIconOptional.isPresent()) {
+            pkgIconImage = pkgIconOptional.get().getPkgIconImage().get();
         }
         else {
             PkgIcon pkgIcon = context.newObject(PkgIcon.class);
             pkg.addToManyTarget(Pkg.PKG_ICONS_PROPERTY, pkgIcon, true);
-            pkgIcon.setMediaType(png);
-            pkgIcon.setSize(size.width);
+            pkgIcon.setMediaType(mediaType);
+            pkgIcon.setSize(size);
             pkgIconImage = context.newObject(PkgIconImage.class);
pkgIcon.addToManyTarget(PkgIcon.PKG_ICON_IMAGES_PROPERTY, pkgIconImage, true);
+            pkgIconOptional = Optional.of(pkgIcon);
         }

-        pkgIconImage.setData(pngData);
+        pkgIconImage.setData(imageData);
         pkg.setModifyTimestamp(new java.util.Date());

- logger.info("the icon {}px for package {} has been updated", size.width, pkg.getName());
+        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());
+        }
+
+        return pkgIconOptional.get();
     }

     // ------------------------------
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgIconController.java Tue Feb 18 10:16:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgIconController.java Mon Feb 24 08:20:27 2014 UTC
@@ -7,21 +7,23 @@

 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+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.haikuos.haikudepotserver.pkg.model.SizeLimitReachedException;
-import org.haikuos.haikudepotserver.security.model.Permission;
+import org.haikuos.haikudepotserver.dataobjects.Pkg;
+import org.haikuos.haikudepotserver.dataobjects.PkgIcon;
+import org.haikuos.haikudepotserver.pkg.PkgService;
 import org.haikuos.haikudepotserver.security.AuthorizationService;
 import org.haikuos.haikudepotserver.support.ByteCounterOutputStream;
+import org.haikuos.haikudepotserver.support.Closeables;
 import org.haikuos.haikudepotserver.support.NoOpOutputStream;
+import org.haikuos.haikudepotserver.support.web.AbstractController;
import org.haikuos.haikudepotserver.web.controller.WebResourceGroupController;
-import org.haikuos.haikudepotserver.dataobjects.Pkg;
-import org.haikuos.haikudepotserver.dataobjects.User;
-import org.haikuos.haikudepotserver.pkg.PkgService;
-import org.haikuos.haikudepotserver.pkg.model.BadPkgIconException;
-import org.haikuos.haikudepotserver.support.web.AbstractController;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
@@ -29,9 +31,9 @@
 import org.springframework.web.bind.annotation.*;

 import javax.annotation.Resource;
-import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.io.InputStream;

 /**
* <p>This controller vends the package icon. This may be provided by data stored in the database, or it may be
@@ -49,6 +51,7 @@
     public final static String KEY_PKGNAME = "pkgname";
     public final static String KEY_FORMAT = "format";
     public final static String KEY_SIZE = "s";
+    public final static String KEY_FALLBACK = "f";

     @Resource
     ServerRuntime serverRuntime;
@@ -59,152 +62,136 @@
     @Resource
     AuthorizationService authorizationService;

- @RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.HEAD)
-    public void fetchHead(
-            HttpServletRequest request,
+ private LoadingCache<Integer, byte[]> genericBitmapIcons = CacheBuilder.newBuilder()
+            .maximumSize(10)
+            .build(
+                    new CacheLoader<Integer, byte[]>() {
+                        public byte[] load(Integer size) {
+ String resource = String.format("/img/generic/generic%d.png", size);
+                            InputStream inputStream = null;
+
+                            try {
+ inputStream = this.getClass().getResourceAsStream(resource);
+
+                                if(null==inputStream) {
+ throw new IllegalStateException(String.format("the resource; %s was not able to be found, but should be in the application build product", resource));
+                                }
+                                else {
+ return ByteStreams.toByteArray(inputStream);
+                                }
+                            }
+                            catch(IOException ioe) {
+ throw new IllegalStateException("unable to read the generic icon",ioe);
+                            }
+                            finally {
+                                Closeables.closeQuietly(inputStream);
+                            }
+                        }
+                    });
+
+    private void handleHeadOrGet(
+            RequestMethod requestMethod,
             HttpServletResponse response,
-            @RequestParam(value = KEY_SIZE, required = true) int size,
-            @PathVariable(value = KEY_FORMAT) String format,
-            @PathVariable(value = KEY_PKGNAME) String pkgName)
+            Integer size,
+            String format,
+            String pkgName,
+            Boolean fallback)
             throws IOException {

-        if(size != 16 && size != 32) {
-            throw new BadSize();
-        }
-
if(Strings.isNullOrEmpty(pkgName) | | !Pkg.NAME_PATTERN.matcher(pkgName).matches()) {
             throw new MissingPkgName();
         }

-        if(Strings.isNullOrEmpty(format) || !"png".equals(format)) {
+        ObjectContext context = serverRuntime.getContext();
+ Optional<org.haikuos.haikudepotserver.dataobjects.MediaType> mediaTypeOptional + = org.haikuos.haikudepotserver.dataobjects.MediaType.getByExtension(context, format);
+
+        if(!mediaTypeOptional.isPresent()) {
+ logger.warn("attempt to request icon of format '{}', but this format was not recognized",format);
             throw new MissingOrBadFormat();
         }

-        ObjectContext context = serverRuntime.getContext();
         Optional<Pkg> pkg = Pkg.getByName(context, pkgName);

         if(!pkg.isPresent()) {
+ logger.info("request for icon for package '{}', but no such package was able to be found",pkgName);
             throw new PkgNotFound();
         }

ByteCounterOutputStream byteCounter = new ByteCounterOutputStream(new NoOpOutputStream()); + Optional<PkgIcon> pkgIconOptional = pkg.get().getPkgIcon(mediaTypeOptional.get(), size);

-        pkgService.writePkgIconImage(
-                byteCounter,
-                context,
-                pkg.get(),
-                size);
+        if(!pkgIconOptional.isPresent()) {

- response.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(byteCounter.getCounter()));
-        response.setContentType(MediaType.PNG.toString());
- response.setDateHeader(HttpHeaders.LAST_MODIFIED, pkg.get().getModifyTimestampSecondAccuracy().getTime()); + // if there is no icon, under very specific circumstances, it may be possible to provide one.

-    }
+            if(
+                    (null!=fallback)
+                            && fallback.booleanValue()
+ && mediaTypeOptional.get().getCode().equals(MediaType.PNG.toString())
+                            && (null!=size)
+                            && (16==size || 32==size)) {

- @RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.GET)
-    public void fetchGet(
-            HttpServletRequest request,
-            HttpServletResponse response,
-            @RequestParam(value = KEY_SIZE, required = true) int size,
-            @PathVariable(value = KEY_FORMAT) String format,
-            @PathVariable(value = KEY_PKGNAME) String pkgName)
-            throws IOException {
+                byte[] data = genericBitmapIcons.getUnchecked(size);
+ response.setHeader(HttpHeaders.CONTENT_LENGTH,Integer.toString(data.length));
+                response.setContentType(mediaTypeOptional.get().getCode());

-        if(size != 16 && size != 32) {
-            throw new BadSize();
-        }
+                if(requestMethod == RequestMethod.GET) {
+                    response.getOutputStream().write(data);
+                }

- if(Strings.isNullOrEmpty(pkgName) | | !Pkg.NAME_PATTERN.matcher(pkgName).matches()) {
-            throw new MissingPkgName();
-        }
+            }
+            else {
+                throw new PkgIconNotFound();
+            }

-        if(Strings.isNullOrEmpty(format) || !"png".equals(format)) {
-            throw new MissingOrBadFormat();
         }
-
-        ObjectContext context = serverRuntime.getContext();
-        Optional<Pkg> pkg = Pkg.getByName(context, pkgName);
+        else {

-        if(!pkg.isPresent()) {
-            throw new PkgNotFound();
-        }
+ byte[] data = pkgIconOptional.get().getPkgIconImage().get().getData();

- if(!Strings.isNullOrEmpty(request.getHeader(HttpHeaders.IF_MODIFIED_SINCE))) { - long value = (request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE) / 1000) * 1000; // round to second - long lastModified = pkg.get().getModifyTimestampSecondAccuracy().getTime(); + response.setHeader(HttpHeaders.CONTENT_LENGTH,Integer.toString(data.length));
+            response.setContentType(mediaTypeOptional.get().getCode());
+ response.setDateHeader(HttpHeaders.LAST_MODIFIED, pkg.get().getModifyTimestampSecondAccuracy().getTime());

-            if(lastModified <= value) {
-                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
-                return;
+            if(requestMethod == RequestMethod.GET) {
+                response.getOutputStream().write(data);
             }
         }
-
-        response.setContentType(MediaType.PNG.toString());
-        response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600");
- response.setDateHeader(HttpHeaders.LAST_MODIFIED, pkg.get().getModifyTimestampSecondAccuracy().getTime());
+    }

-        pkgService.writePkgIconImage(
-                response.getOutputStream(),
-                context,
-                pkg.get(),
-                size);
+ @RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.HEAD)
+    public void handleHead(
+            HttpServletResponse response,
+            @RequestParam(value = KEY_SIZE) Integer size,
+            @PathVariable(value = KEY_FORMAT) String format,
+            @PathVariable(value = KEY_PKGNAME) String pkgName,
+            @RequestParam(value = KEY_FALLBACK) Boolean fallback)
+            throws IOException {
+        handleHeadOrGet(
+                RequestMethod.HEAD,
+                response,
+                size,
+                format,
+                pkgName,
+                fallback);
     }

-    /**
- * <p>This handler will take-up an HTTP PUT that provides an icon image for the package.</p>
-     */
-
- @RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.PUT)
-    public void put(
-            HttpServletRequest request,
+ @RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.GET)
+    public void handleGet(
             HttpServletResponse response,
- @RequestParam(value = KEY_SIZE, required = true) int expectedSize,
+            @RequestParam(value = KEY_SIZE, required = true) int size,
             @PathVariable(value = KEY_FORMAT) String format,
- @PathVariable(value = KEY_PKGNAME) String pkgName) throws IOException {
-
- if(Strings.isNullOrEmpty(pkgName) | | !Pkg.NAME_PATTERN.matcher(pkgName).matches()) {
-            throw new MissingPkgName();
-        }
-
-        if(Strings.isNullOrEmpty(format) || !"png".equals(format)) {
-            throw new MissingOrBadFormat();
-        }
-
-        ObjectContext context = serverRuntime.getContext();
-
-        Optional<Pkg> pkg = Pkg.getByName(context, pkgName);
-
-        if(!pkg.isPresent()) {
-            throw new PkgNotFound();
-        }
-
-        // check the authorization
-
-        Optional<User> user = tryObtainAuthenticatedUser(context);
-
- if(!authorizationService.check(context, user.orNull(), pkg.get(), Permission.PKG_EDITICON)) { - logger.warn("attempt to edit the pkg icon, but there is no user present or that user is not able to edit the pkg");
-            throw new PkgAuthorizationFailure();
-        }
-
-        try {
-            pkgService.storePkgIconImage(
-                    request.getInputStream(),
-                    expectedSize,
-                    context,
-                    pkg.get());
-        }
-        catch(SizeLimitReachedException sizeLimit) {
- logger.warn("attempt to load in an icon file that is larger than that allowed");
-            throw new MissingOrBadFormat();
-        }
-        catch(BadPkgIconException badIcon) {
-            throw new MissingOrBadFormat();
-        }
-
-        context.commitChanges();
-
-        response.setStatus(HttpServletResponse.SC_OK);
+            @PathVariable(value = KEY_PKGNAME) String pkgName,
+            @RequestParam(value = KEY_FALLBACK) Boolean fallback)
+    throws IOException {
+        handleHeadOrGet(
+                RequestMethod.GET,
+                response,
+                size,
+                format,
+                pkgName,
+                fallback);
     }

// these are the various errors that can arise in supplying or providing a package icon.
@@ -221,7 +208,7 @@
@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="the requested package was unable to found")
     public class PkgNotFound extends RuntimeException {}

- @ResponseStatus(value= HttpStatus.UNAUTHORIZED, reason="the requested package cannot be edited by the authenticated user")
-    public class PkgAuthorizationFailure extends RuntimeException {}
+ @ResponseStatus(value= HttpStatus.NOT_FOUND, reason="the requested package icon was unable to found")
+    public class PkgIconNotFound extends RuntimeException {}

 }
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/ImageHelper.java Wed Feb 12 10:07:08 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/ImageHelper.java Mon Feb 24 08:20:27 2014 UTC
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013, Andrew Lindesay
+ * Copyright 2013-2014, Andrew Lindesay
  * Distributed under the terms of the MIT License.
  */

@@ -13,13 +13,40 @@

protected static Logger logger = LoggerFactory.getLogger(ImageHelper.class);

-    private int MAGIC[] = {
+    private int HVIF_MAGIC[] = {
+            0x6e, 0x63, 0x69, 0x66
+    };
+
+    private int PNG_MAGIC[] = {
             0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
     };

-    private int IHDR[] = {
+    private int PNG_IHDR[] = {
             0x49, 0x48, 0x44, 0x52
     };
+
+    /**
+ * <p>Haiku Vector Icon Format (hvif) is a data format specific to the Haiku platform and is a way in which + * icons can be stored very compactly in a vector format. This method will return true if the data supplied
+     * looks like hvif.</p>
+     */
+
+    public boolean looksLikeHaikuVectorIconFormat(byte[] data) {
+        Preconditions.checkNotNull(data);
+
+        if(data.length < 4) {
+            return false;
+        }
+
+        for(int i=0;i< HVIF_MAGIC.length;i++) {
+            if((int) (0xff & data[i]) != HVIF_MAGIC[i]) {
+ logger.trace("the magic header is not present in the hvif data");
+                return false;
+            }
+        }
+
+        return true;
+    }

     /**
* <p>This method will read the first few bytes of a PNG image and will return the size. It will return
@@ -35,8 +62,8 @@

         // check for the magic header.

-        for(int i=0;i<MAGIC.length;i++) {
-            if((int) (0xff & data[i]) != MAGIC[i]) {
+        for(int i=0;i< PNG_MAGIC.length;i++) {
+            if((int) (0xff & data[i]) != PNG_MAGIC[i]) {
logger.trace("the magic header is not present in the png data");
                 return null;
             }
@@ -48,8 +75,8 @@

         // check for the expected first chunk header.

-        for(int i=0;i<IHDR.length;i++) {
-            if((int) (0xff & data[12+i]) != IHDR[i]) {
+        for(int i=0;i<PNG_IHDR.length;i++) {
+            if((int) (0xff & data[12+i]) != PNG_IHDR[i]) {
logger.trace("the IHDR chunk is not present in the png data");
                 return null;
             }
=======================================
--- /haikudepotserver-webapp/src/main/resources/messages.properties Tue Feb 18 00:02:17 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/messages.properties Mon Feb 24 08:20:27 2014 UTC
@@ -27,12 +27,13 @@
 banner.userActions.createUser=Create User
 banner.userActions.details=User Details

-editPkgIcon.icon32File.required=The 32x32px version of the icon is required. -editPkgIcon.icon32File.badsize=The file is too large or too small to be a PNG image suitable for this icon. -editPkgIcon.icon32File.badformatorsize=The file is either not in PNG format, or is not of the correct dimensions for the package icon. -editPkgIcon.icon16File.required=The 16x16px version of the icon is required. -editPkgIcon.icon16File.badsize=The file is too large or too small to be a PNG image suitable for this icon. -editPkgIcon.icon16File.badformatorsize=The file is either not in PNG format, or is not of the correct dimensions for the package icon. +editPkgIcon.iconBitmap32File.required=The 32x32px version of the icon is required. +editPkgIcon.iconBitmap32File.badsize=The file is too large or too small to be a PNG image suitable for this icon. +editPkgIcon.iconBitmap32File.badformatorsize=The file is either not in PNG format, or is not of the correct dimensions for the package icon. +editPkgIcon.iconBitmap16File.required=The 16x16px version of the icon is required. +editPkgIcon.iconBitmap16File.badsize=The file is too large or too small to be a PNG image suitable for this icon. +editPkgIcon.iconBitmap16File.badformatorsize=The file is either not in PNG format, or is not of the correct dimensions for the package icon. +editPkgIcon.iconHvifFile.badformatorsize=The file is not in a suitable HVIF format.

 home.viewCriteriaType.all=All
 home.viewCriteriaType.search=Search
=======================================
--- /haikudepotserver-webapp/src/main/resources/spring/servlet-context.xml Wed Feb 19 10:33:16 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/spring/servlet-context.xml Mon Feb 24 08:20:27 2014 UTC
@@ -18,7 +18,7 @@
     <mvc:annotation-driven/>

     <mvc:resources mapping="/webjars/**" location="/webjars/" />
-    <mvc:resources mapping="/js/**" location="/js/" />
+    <mvc:resources mapping="/js/**" location="/js/" cache-period="0"/>
     <mvc:resources mapping="/css/**" location="/css/" />
     <mvc:resources mapping="/img/**" location="/img/" />
     <mvc:resources mapping="favicon.ico" location="/img/favicon.ico" />
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/constants.js Sun Feb 2 09:15:35 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/constants.js Mon Feb 24 08:20:27 2014 UTC
@@ -11,7 +11,9 @@
         ENDPOINT_API_V1_PKG : '/api/v1/pkg',
         ENDPOINT_API_V1_CAPTCHA : '/api/v1/captcha',
         ENDPOINT_API_V1_MISCELLANEOUS : '/api/v1/miscellaneous',
-        ENDPOINT_API_V1_USER : '/api/v1/user'
+        ENDPOINT_API_V1_USER : '/api/v1/user',

+        MEDIATYPE_PNG : 'image/png',
+        MEDIATYPE_HAIKUVECTORICONFILE : 'application/x-vnd.haiku-icon'
     }
 );
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgicon.html Thu Jan 16 08:37:44 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgicon.html Mon Feb 24 08:20:27 2014 UTC
@@ -11,16 +11,25 @@
             </div>
         </div>

-        <label for="icon-32-file">Icon 32x32px</label>
- <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('icon32File')"> - <input id="icon-32-file" name="icon32File" type="file" ng-model="editPkgIcon.icon32File" file-supply required></input> - <error-messages key-prefix="editPkgIcon.icon32File" error="editPkgIconForm.icon32File.$error"></error-messages>
+        <label for="icon-bitmap-16-file">Icon 16x16px</label>
+ <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('iconBitmap16File')"> + <input id="icon-bitmap-16-file" name="iconBitmap16File" type="file" ng-model="editPkgIcon.iconBitmap16File" file-supply required></input> + <error-messages key-prefix="editPkgIcon.iconBitmap16File" error="editPkgIconForm.iconBitmap16File.$error"></error-messages>
+        </div>
+
+        <label for="icon-bitmap-32-file">Icon 32x32px</label>
+ <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('iconBitmap32File')"> + <input id="icon-bitmap-32-file" name="iconBitmap32File" type="file" ng-model="editPkgIcon.iconBitmap32File" file-supply required></input> + <error-messages key-prefix="editPkgIcon.iconBitmap32File" error="editPkgIconForm.iconBitmap32File.$error"></error-messages>
         </div>

-        <label for="icon-16-file">Icon 16x16px</label>
- <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('icon16File')"> - <input id="icon-16-file" name="icon16File" type="file" ng-model="editPkgIcon.icon16File" file-supply required></input> - <error-messages key-prefix="editPkgIcon.icon16File" error="editPkgIconForm.icon16File.$error"></error-messages>
+        <label for="icon-hvif-file">Icon Hvif</label>
+ <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('iconHvifFile')"> + <input id="icon-hvif-file" name="iconHvifFile" type="file" ng-model="editPkgIcon.iconHvifFile" file-supply></input>
+            <span ng-show="editPkgIcon.iconHvifFile">
+                (<a href="" ng-click="goClearIconHvifFile()">Clear</a>)
+            </span>
+ <error-messages key-prefix="editPkgIcon.iconHvifFile" error="editPkgIconForm.iconHvifFile.$error"></error-messages>
         </div>

         <div class="form-action-container">
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgiconcontroller.js Tue Feb 18 09:54:04 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgiconcontroller.js Mon Feb 24 08:20:27 2014 UTC
@@ -20,8 +20,9 @@
             $scope.pkg = undefined;
             $scope.amSaving = false;
             $scope.editPkgIcon = {
-                icon16File : undefined,
-                icon32File : undefined
+                iconBitmap16File : undefined, // bitmap
+                iconBitmap32File : undefined, // bitmap
+ iconHvifFile : undefined // vector 'Haiku Vector Icon Format'
             };

             $scope.shouldSpin = function() {
@@ -74,27 +75,40 @@
// the server because the user will not know if an updated file is also bad until the server has seen it; // ie: the validation is happening server-side rather than client-side.

-            function validateIconFile(file, model) {
+            function validateBitmapIconFile(file, model) {
                 model.$setValidity('badformatorsize',true);
model.$setValidity('badsize',undefined==file || (file.size
24 && file.size < ICON_SIZE_LIMIT));
             }

-            function icon32FileDidChange() {
- validateIconFile($scope.editPkgIcon.icon32File, $scope.editPkgIconForm['icon32File']);
+            function validateHvifIconFile(file, model) {
+                model.$setValidity('badformatorsize',true);
+ model.$setValidity('badsize',undefined==file || (file.size
4 && file.size < ICON_SIZE_LIMIT));
             }

-            function icon16FileDidChange() {
- validateIconFile($scope.editPkgIcon.icon16File, $scope.editPkgIconForm['icon16File']);
+            function iconBitmap32FileDidChange() {
+ validateBitmapIconFile($scope.editPkgIcon.iconBitmap32File, $scope.editPkgIconForm['iconBitmap32File']);
             }

-            $scope.$watch('editPkgIcon.icon32File', function(newValue) {
-                icon32FileDidChange();
+            function iconBitmap16FileDidChange() {
+ validateBitmapIconFile($scope.editPkgIcon.iconBitmap16File, $scope.editPkgIconForm['iconBitmap16File']);
+            }
+
+ $scope.$watch('editPkgIcon.iconBitmap32File', function(newValue) {
+                iconBitmap32FileDidChange();
             });

-            $scope.$watch('editPkgIcon.icon16File', function(newValue) {
-                icon16FileDidChange();
+ $scope.$watch('editPkgIcon.iconBitmap16File', function(newValue) {
+                iconBitmap16FileDidChange();
             });

+            $scope.$watch('editPkgIcon.iconHvifFile', function(newValue) {
+ validateHvifIconFile($scope.editPkgIcon.iconHvifFile, $scope.editPkgIconForm['iconHvifFile']);
+            });
+
+            $scope.goClearIconHvifFile = function() {
+                $scope.editPkgIcon.iconHvifFile = undefined;
+            }
+
// This function will take the data from the form and load in the new pkg icons

             $scope.goStorePkgIcons = function() {
@@ -105,42 +119,164 @@

                 $scope.amSaving = true;

- // two PUT requests are made to the server in order to convey the PNG data. + function handleStorePkgIcons(base64IconBitmap16, base64IconBitmap32, base64IconHvif) {

- pkgIcon.setPkgIcon($scope.pkg, $scope.editPkgIcon.icon16File,16).then(
-                    function() {
- $log.info('have set the 16px icon for the pkg '+$scope.pkg.name);
+                    var pkgIcons = [
+                        {
+                            mediaTypeCode : constants.MEDIATYPE_PNG,
+                            size : 16,
+                            dataBase64 : base64IconBitmap16
+                        },
+                        {
+                            mediaTypeCode : constants.MEDIATYPE_PNG,
+                            size : 32,
+                            dataBase64 : base64IconBitmap32
+                        }
+                    ];

- pkgIcon.setPkgIcon($scope.pkg, $scope.editPkgIcon.icon32File,32).then(
-                            function() {
-                                $scope.amSaving = false;
- $log.info('have set the 32px icon for the pkg '+$scope.pkg.name); - $location.path('/viewpkg/'+$routeParams.name+'/'+$routeParams.version+'/'+$routeParams.architectureCode).search({});
-                            },
-                            function(e) {
-                                $scope.amSaving = false;
- if(e==pkgIcon.errorCodes.BADFORMATORSIZEERROR) { - $scope.editPkgIconForm['icon32File'].$setValidity('badformatorsize',false);
-                                }
-                                else {
- $log.error('unable to set the 32px icon for the pkg '+$scope.pkg.name);
-                                    $location.path('/error').search({});
-                                }
+                    if(base64IconHvif) {
+                        pkgIcons.push({
+ mediaTypeCode : constants.MEDIATYPE_HAIKUVECTORICONFILE,
+                            dataBase64 : base64IconHvif
+                        });
+                    }
+
+                    jsonRpc.call(
+                            constants.ENDPOINT_API_V1_PKG,
+                            "configurePkgIcon",
+                            [{
+                                pkgName: $routeParams.name,
+                                pkgIcons: pkgIcons
+                            }]
+                        ).then(
+                        function(result) {
+ $log.info('have updated the pkg icons for pkg '+$scope.pkg); + $location.path('/viewpkg/'+$routeParams.name+'/'+$routeParams.version+'/'+$routeParams.architectureCode).search({});
+                            $scope.amSaving = false;
+                        },
+                        function(err) {
+
+                            switch(err.code) {
+
+ // the inbound error may involve reporting on bad data. If this is the case then the error + // should be reverse mapped to the input field.
+
+                                case jsonRpc.errorCodes.BADPKGICON:
+
+                                    if(err.data) {
+                                        switch(err.data.mediaTypeCode) {
+
+                                            case constants.MEDIATYPE_PNG:
+                                                switch(err.data.size) {
+                                                    case 16:
+ $scope.editPkgIconForm['iconBitmap16File'].$setValidity('badformatorsize',false);
+                                                        break;
+
+                                                    case 32:
+ $scope.editPkgIconForm['iconBitmap32File'].$setValidity('badformatorsize',false);
+                                                        break;
+
+                                                    default:
+ throw 'expected size; ' + error.data.size;
+                                                }
+                                                break;
+
+ case constants.MEDIATYPE_HAIKUVECTORICONFILE: + $scope.editPkgIconForm['iconHvifFile'].$setValidity('badformatorsize',false);
+                                                break;
+
+                                            default:
+ throw 'unexpected media type code; ' + err.data.mediaTypeCode;
+
+                                        }
+                                    }
+                                    else {
+ throw 'expected data to be supplied with a bad pkg icon';
+                                    }
+
+                                    break;
+
+                                default:
+                                    errorHandling.handleJsonRpcError(err);
+                                    break;
+
                             }
-                        )
-                    },
-                    function(e) {
-                        $scope.amSaving = false;
-                        if(e==pkgIcon.errorCodes.BADFORMATORSIZEERROR) {
- $scope.editPkgIconForm['icon16File'].$setValidity('badformatorsize',false);
+
+                            $scope.amSaving = false;
+
                         }
-                        else {
- $log.error('unable to set the 16px icon for the pkg '+$scope.pkg.name);
-                            $location.path('/error').search({});
+                    );
+
+                }
+
+                // pull in all of the data as base64-ized data URLs
+
+                var readerIconBitmap16 = new FileReader();
+                var readerIconBitmap32 = new FileReader();
+ var readerHvif = $scope.editPkgIcon.iconHvifFile ? new FileReader() : undefined;
+
+                function checkHasCompletedFileReaderProcessing() {
+
+ // data urls can come in a number of forms. This function will strip ut the data material and + // just get at the base64. If the data is not base64, it will throw an exception. Maybe a more
+                    // elaborate handling will be required?
+
+                    function dataUrlToBase64(u) {
+
+                        if(!u) {
+ throw 'the data url must be supplied to convert to base64';
                         }
+
+                        if(0!= u.indexOf('data:')) {
+ throw 'the data url was unable to be converted to base64 because it does not look like a data url';
+                        }
+
+                        var commaI = u.indexOf(',');
+
+                        if(-1==commaI) {
+ throw 'expecting comma in data url to preceed the base64 data';
+                        }
+
+ if(!_.indexOf(u.substring(5,commaI).split(';'),'base64')) { + throw 'expecting base64 to appear in the data url';
+                        }
+
+                        return u.substring(commaI+1);
                     }
-                );
-            }
+
+                   if(2==readerIconBitmap16.readyState
+                       && 2==readerIconBitmap32.readyState
+                       && (!readerHvif || 2==readerHvif.readyState)) {
+
+                       handleStorePkgIcons(
+                           dataUrlToBase64(readerIconBitmap16.result),
+                           dataUrlToBase64(readerIconBitmap32.result),
+ readerHvif ? dataUrlToBase64(readerHvif.result) : null);
+                   }
+                }
+
+                readerIconBitmap16.onloadend = function() {
+                    checkHasCompletedFileReaderProcessing();
+                }
+
+ readerIconBitmap16.readAsDataURL($scope.editPkgIcon.iconBitmap16File);
+
+                readerIconBitmap32.onloadend = function() {
+                    checkHasCompletedFileReaderProcessing();
+                }
+
+ readerIconBitmap32.readAsDataURL($scope.editPkgIcon.iconBitmap32File);
+
+                if($scope.editPkgIcon.iconHvifFile) {
+
+                    readerHvif.onloadend = function() {
+                        checkHasCompletedFileReaderProcessing();
+                    }
+
+ readerHvif.readAsDataURL($scope.editPkgIcon.iconHvifFile);
+                }
+
+            } // goStorePkgIcons

         }
     ]
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Tue Feb 18 00:17:42 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Mon Feb 24 08:20:27 2014 UTC
@@ -158,7 +158,7 @@
                 jsonRpc.call(
                         constants.ENDPOINT_API_V1_PKG,
                         "removePkgIcon",
-                        [{ name: $routeParams.name }]
+                        [{ pkgName: $routeParams.name }]
                     ).then(
                     function(result) {
$log.info('removed icons for '+$routeParams.name+' pkg');
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/directive/filesupplydirective.js Thu Dec 5 09:23:22 2013 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/filesupplydirective.js Mon Feb 24 08:20:27 2014 UTC
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013, Andrew Lindesay
+ * Copyright 2013-2014, Andrew Lindesay
  * Distributed under the terms of the MIT License.
  */

@@ -17,11 +17,23 @@
             restrict: 'A',
             replace: true,
             link: function(scope, elem, attrs, ngModel) {
-               elem.on('change', function() {
-                   scope.$apply(function() {
-                       ngModel.$setViewValue(elem[0].files[0]);
-                   });
+
+                elem.on('change', function() {
+                    scope.$apply(function() {
+                        ngModel.$setViewValue(elem[0].files[0]);
+                    });
                 });
+
+                scope.$watch(
+                    function() {
+                        return ngModel.$viewValue
+                    },
+                    function(oldValue, newValue) {
+                        if(!oldValue && newValue) {
+                            elem.val('');
+                        }
+                    }
+                );
             }
         };
     }
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/service/jsonrpcservice.js Thu Dec 5 09:23:22 2013 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/service/jsonrpcservice.js Mon Feb 24 08:20:27 2014 UTC
@@ -1,5 +1,5 @@
 /*
- * Copyright 2013, Andrew Lindesay
+ * Copyright 2013-2014, Andrew Lindesay
  * Distributed under the terms of the MIT License.
  */

@@ -28,7 +28,8 @@
                     VALIDATION : -32800,
                     OBJECTNOTFOUND : -32801,
                     CAPTCHABADRESPONSE : -32802,
-                    AUTHORIZATIONFAILURE : -32803
+                    AUTHORIZATIONFAILURE : -32803,
+                    BADPKGICON : -32804
                 },

                 /**
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgiconservice.js Tue Feb 18 00:02:17 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgiconservice.js Mon Feb 24 08:20:27 2014 UTC
@@ -9,8 +9,8 @@

 angular.module('haikudepotserver').factory('pkgIcon',
     [
-        '$log','$q','$http',
-        function($log,$q,$http) {
+        '$log','$q','$http','constants',
+        function($log,$q,$http,constants) {

             var PkgIcon = {

@@ -52,9 +52,28 @@

// this function will set the icon for the package. Note that there may be more than one variant and // this method will need to be invoked for each variant; or example 16px or 32px versions of the same
-                // package icon.
+ // package icon. It is also possible to store a vector (hvif) file of the icon as well.

-                setPkgIcon : function(pkg, iconFile, expectedSize) {
+ setPkgIcon : function(pkg, mediaType, iconFile, expectedSize) {
+
+ // this function will check to see if the string 's' ends with the string 'e'.
+
+                    function endsWith(s, e) {
+                        return -1 != s.indexOf(e, s.length - e.length);
+                    }
+
+                    function mediaTypeToExtension(m) {
+                        switch(m) {
+                            case constants.MEDIATYPE_HAIKUVECTORICONFILE:
+                                return 'hvif';
+
+                            case constants.MEDIATYPE_PNG:
+                                return 'png';
+
+                            default:
+                                throw 'unknown media type; '+m;
+                        }
+                    }

                     if(!pkg) {
throw 'the pkg must be supplied to set the pkg icon';
@@ -64,18 +83,49 @@
throw 'to set the pkg icon for '+pkg.name+' the image file must be provided';
                     }

- if(!expectedSize || !(16==expectedSize || 32==expectedSize)) {
-                        throw 'the expected size must be 16 or 32px';
+ // if there is no media type supplied then try to derive on from the file extension.
+
+                    if(!mediaType || !mediaType.length) {
+                        if(endsWith(iconFile.name,'.png')) {
+                            mediaType = constants.MEDIATYPE_PNG;
+                        }
+
+                        if(endsWith(iconFile.name,'.hvif')) {
+ mediaType = constants.MEDIATYPE_HAIKUVECTORICONFILE;
+                        }
+
+                        if(!mediaType) {
+ throw 'unable to derive a media type from the file name; ' + iconFile.name;
+                        }
+                    }
+
+                    var isBitmap = mediaType == constants.MEDIATYPE_PNG;
+
+                    if(isBitmap) {
+ if(!expectedSize || !(16==expectedSize || 32==expectedSize)) {
+                            throw 'the expected size must be 16 or 32px';
+                        }
+                    }
+                    else {
+                        if(expectedSize) {
+ throw 'the expected size cannot be supplied because the image is vector';
+                        }
                     }

                     var deferred = $q.defer();
+
+ var path = '/pkgicon/'+pkg.name+'.'+mediaTypeToExtension(mediaType);
+
+                    if(expectedSize) {
+                        path += '?s=' + expectedSize;
+                    }

                     $http({
                         cache: false,
                         method: 'PUT',
-                        url: '/pkgicon/'+pkg.name+'.png?s='+expectedSize,
+                        url: path,
                         headers: _.extend(
-                            { 'Content-Type' : 'image/png' },
+                            { 'Content-Type' : mediaType },
                             PkgIcon.headers),
                         data: iconFile
                     })
@@ -123,7 +173,7 @@
throw 'the size is not valid for obtaining the package icon url';
                     }

-                    var u = '/pkgicon/' + pkg.name + '.png?s=' + size;
+ var u = '/pkgicon/' + pkg.name + '.png?s=' + size + '&f=true';

                     if(pkg.modifyTimestamp) {
                         u += '&m=' + pkg.modifyTimestamp;
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/PkgApiIT.java Tue Feb 18 10:09:54 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/PkgApiIT.java Mon Feb 24 08:20:27 2014 UTC
@@ -9,13 +9,17 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.net.MediaType;
+import com.googlecode.jsonrpc4j.Base64;
 import junit.framework.Assert;
 import org.apache.cayenne.ObjectContext;
 import org.fest.assertions.Assertions;
 import org.haikuos.haikudepotserver.api1.PkgApi;
 import org.haikuos.haikudepotserver.api1.model.pkg.*;
+import org.haikuos.haikudepotserver.api1.support.BadPkgIconException;
 import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException;
 import org.haikuos.haikudepotserver.dataobjects.Pkg;
+import org.haikuos.haikudepotserver.dataobjects.PkgIcon;
 import org.haikuos.haikudepotserver.dataobjects.PkgScreenshot;
 import org.haikuos.haikudepotserver.pkg.PkgService;
 import org.haikuos.haikudepotserver.support.Closeables;
@@ -106,6 +110,116 @@
Assert.fail("expected an instance of "+ObjectNotFoundException.class.getSimpleName()+" to be thrown, but "+th.getClass().getSimpleName()+" was instead");
         }
     }
+
+    /**
+     * <p>Here we are trying to load the HVIF data in as PNG images.</p>
+     */
+
+    @Test
+    public void testConfigurePkgIcon_badData() throws Exception {
+
+        setAuthenticatedUserToRoot();
+ IntegrationTestSupportService.StandardTestData data = integrationTestSupportService.createStandardTestData();
+        byte[] sample16 = getResourceData("/sample-16x16.png");
+        byte[] sample32 = getResourceData("/sample-32x32.png");
+        byte[] sampleHvif = getResourceData("/sample.hvif");
+
+        ConfigurePkgIconRequest request = new ConfigurePkgIconRequest();
+
+        request.pkgName = "pkg1";
+        request.pkgIcons = ImmutableList.of(
+                new ConfigurePkgIconRequest.PkgIcon(
+                        MediaType.PNG.toString(),
+                        16,
+                        Base64.encodeBytes(sampleHvif)),
+                new ConfigurePkgIconRequest.PkgIcon(
+                        MediaType.PNG.toString(),
+                        32,
+                        Base64.encodeBytes(sampleHvif)),
+                new ConfigurePkgIconRequest.PkgIcon(
+ org.haikuos.haikudepotserver.dataobjects.MediaType.MEDIATYPE_HAIKUVECTORICONFILE,
+                        null,
+                        Base64.encodeBytes(sampleHvif)));
+
+        try {
+
+            // ------------------------------------
+            pkgApi.configurePkgIcon(request);
+            // ------------------------------------
+
+ Assert.fail("expected an instance of '"+BadPkgIconException.class.getSimpleName()+"' to have been thrown");
+
+        }
+        catch(BadPkgIconException bpie) {
+
+ // This is the first one that failed so we should get this come up as the exception that was thrown.
+
+            Assertions.assertThat(bpie.getSize()).isEqualTo(16);
+ Assertions.assertThat(bpie.getMediaTypeCode()).isEqualTo(MediaType.PNG.toString());
+        }
+    }
+
+
+    /**
+     * <p>This test will configure the icons for the package.</p>
+     */
+
+    @Test
+    public void testConfigurePkgIcon_ok() throws Exception {
+
+        setAuthenticatedUserToRoot();
+ IntegrationTestSupportService.StandardTestData data = integrationTestSupportService.createStandardTestData();
+        byte[] sample16 = getResourceData("/sample-16x16.png");
+        byte[] sample32 = getResourceData("/sample-32x32.png");
+        byte[] sampleHvif = getResourceData("/sample.hvif");
+
+        ConfigurePkgIconRequest request = new ConfigurePkgIconRequest();
+
+        request.pkgName = "pkg1";
+        request.pkgIcons = ImmutableList.of(
+                new ConfigurePkgIconRequest.PkgIcon(
+                        MediaType.PNG.toString(),
+                        16,
+                        Base64.encodeBytes(sample16)),
+                new ConfigurePkgIconRequest.PkgIcon(
+                        MediaType.PNG.toString(),
+                        32,
+                        Base64.encodeBytes(sample32)),
+                new ConfigurePkgIconRequest.PkgIcon(
+ org.haikuos.haikudepotserver.dataobjects.MediaType.MEDIATYPE_HAIKUVECTORICONFILE,
+                        null,
+                        Base64.encodeBytes(sampleHvif)));
+
+        // ------------------------------------
+        pkgApi.configurePkgIcon(request);
+        // ------------------------------------
+
+        {
+            ObjectContext objectContext = serverRuntime.getContext();
+ Optional<Pkg> pkgOptionalafter = Pkg.getByName(objectContext, "pkg1");
+
+            org.haikuos.haikudepotserver.dataobjects.MediaType mediaTypePng
+ = org.haikuos.haikudepotserver.dataobjects.MediaType.getByCode(
+                    objectContext,
+                    MediaType.PNG.toString()).get();
+
+ org.haikuos.haikudepotserver.dataobjects.MediaType mediaTypeHvif + = org.haikuos.haikudepotserver.dataobjects.MediaType.getByCode(
+                    objectContext,
+ org.haikuos.haikudepotserver.dataobjects.MediaType.MEDIATYPE_HAIKUVECTORICONFILE).get();
+
+ Assertions.assertThat(pkgOptionalafter.get().getPkgIcons().size()).isEqualTo(3);
+
+ Optional<PkgIcon> pkgIcon16Optional = pkgOptionalafter.get().getPkgIcon(mediaTypePng, 16); + Assertions.assertThat(pkgIcon16Optional.get().getPkgIconImage().get().getData()).isEqualTo(sample16);
+
+ Optional<PkgIcon> pkgIcon32Optional = pkgOptionalafter.get().getPkgIcon(mediaTypePng, 32); + Assertions.assertThat(pkgIcon32Optional.get().getPkgIconImage().get().getData()).isEqualTo(sample32);
+
+ Optional<PkgIcon> pkgIconHvifOptional = pkgOptionalafter.get().getPkgIcon(mediaTypeHvif, null); + Assertions.assertThat(pkgIconHvifOptional.get().getPkgIconImage().get().getData()).isEqualTo(sampleHvif);
+        }
+    }

     /**
* <p>This test knows that an icon exists for pkg1 and then removes it.</p>
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/support/IntegrationTestSupportService.java Fri Feb 14 08:42:33 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/support/IntegrationTestSupportService.java Mon Feb 24 08:20:27 2014 UTC
@@ -5,12 +5,18 @@

 package org.haikuos.haikudepotsever.api1.support;

+import com.google.common.net.*;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.configuration.server.ServerRuntime;
 import org.haikuos.haikudepotserver.dataobjects.*;
+import org.haikuos.haikudepotserver.dataobjects.MediaType;
 import org.haikuos.haikudepotserver.pkg.PkgService;
 import org.haikuos.haikudepotserver.support.Closeables;
+import org.haikuos.haikudepotserver.web.controller.WebResourceGroupController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
+import sun.util.logging.resources.logging;

 import javax.annotation.Resource;
 import java.io.InputStream;
@@ -22,6 +28,8 @@
 @Service
 public class IntegrationTestSupportService {

+ protected static Logger logger = LoggerFactory.getLogger(IntegrationTestSupportService.class);
+
     @Resource
     ServerRuntime serverRuntime;

@@ -58,7 +66,12 @@

         try {
inputStream = this.getClass().getResourceAsStream(String.format("/sample-%dx%d.png",size,size)); - pkgService.storePkgIconImage(inputStream, size, objectContext, pkg);
+            pkgService.storePkgIconImage(
+                    inputStream,
+ MediaType.getByCode(objectContext, com.google.common.net.MediaType.PNG.toString()).get(),
+                    size,
+                    objectContext,
+                    pkg);
         }
         catch(Exception e) {
throw new IllegalStateException("an issue has arisen loading an icon",e);
@@ -75,6 +88,8 @@

     public StandardTestData createStandardTestData() {

+        logger.info("will create standard test data");
+
         ObjectContext context = getObjectContext();
         StandardTestData result = new StandardTestData();

@@ -141,6 +156,8 @@

         context.commitChanges();

+        logger.info("did create standard test data");
+
         return result;
     }

=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/pkg/controller/PkgIconControllerIT.java Fri Feb 14 08:42:33 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/pkg/controller/PkgIconControllerIT.java Mon Feb 24 08:20:27 2014 UTC
@@ -42,49 +42,20 @@
IntegrationTestSupportService.StandardTestData data = integrationTestSupportService.createStandardTestData();
         byte[] imageData = getIconData();

-        MockHttpServletRequest request = new MockHttpServletRequest();
         MockHttpServletResponse response = new MockHttpServletResponse();

         // ------------------------------------
-        pkgIconController.fetchGet(
-                request, response,
+        pkgIconController.handleGet(
+                response,
                 32,
                 "png",
-                "pkg1");
+                "pkg1",
+                true);
         // -----------------------------------

Assertions.assertThat(response.getContentType()).isEqualTo(MediaType.PNG.toString()); Assertions.assertThat(response.getContentAsByteArray()).isEqualTo(imageData);

     }
-
-    @Test
-    public void testPut() throws Exception {
-
-        setAuthenticatedUserToRoot();
-
- IntegrationTestSupportService.StandardTestData data = integrationTestSupportService.createStandardTestData();
-        byte[] imageData = getIconData();
-
-        MockHttpServletRequest request = new MockHttpServletRequest();
-        request.setContent(imageData);
-        MockHttpServletResponse response = new MockHttpServletResponse();
-
-        // ------------------------------------
-         pkgIconController.put(
-                 request,response,
-                 32,
-                 "png",
-                 "pkg2");
-        // ------------------------------------
-
-        {
-            ObjectContext context = serverRuntime.getContext();
-            Optional<Pkg> pkgOptional = Pkg.getByName(context,"pkg2");
- Optional<org.haikuos.haikudepotserver.dataobjects.MediaType> mediaTypeOptional = org.haikuos.haikudepotserver.dataobjects.MediaType.getByCode(context,MediaType.PNG.toString()); - Assertions.assertThat(pkgOptional.get().getPkgIcon(mediaTypeOptional.get(),32).get().getPkgIconImage().get().getData()).isEqualTo(imageData);
-        }
-
-    }

 }
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/support/ImageHelperTest.java Sat Jan 18 09:59:17 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/support/ImageHelperTest.java Mon Feb 24 08:20:27 2014 UTC
@@ -16,7 +16,7 @@

 public class ImageHelperTest {

-    private byte[] getPngData(String leafname) throws IOException {
+    private byte[] getData(String leafname) throws IOException {
         InputStream inputStream = null;

         try {
@@ -29,7 +29,7 @@
     }

private void assertImageSize(String leafname, int width, int height) throws IOException {
-        byte[] png = getPngData(leafname);
+        byte[] png = getData(leafname);
         ImageHelper imageHelper = new ImageHelper();
         ImageHelper.Size size = imageHelper.derivePngSize(png);
         assertThat(size).isNotNull();
@@ -43,5 +43,19 @@
         assertImageSize("/sample-32x32.png",32,32);
         assertImageSize("/sample-16x16.png",16,16);
     }
+
+    @Test
+ public void testLooksLikeHaikuVectorImageFormat_true() throws IOException {
+         byte[] data = getData("/sample.hvif");
+        ImageHelper imageHelper = new ImageHelper();
+ assertThat(imageHelper.looksLikeHaikuVectorIconFormat(data)).isTrue();
+    }
+
+    @Test
+ public void testLooksLikeHaikuVectorImageFormat_false() throws IOException {
+        byte[] data = getData("/sample-16x16.png");
+        ImageHelper imageHelper = new ImageHelper();
+ assertThat(imageHelper.looksLikeHaikuVectorIconFormat(data)).isFalse();
+    }

 }

==============================================================================
Revision: d14c61e65059
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Mon Feb 24 10:10:13 2014 UTC
Log:      + icon and screenshot handling changes

http://code.google.com/p/haiku-depot-web-app/source/detail?r=d14c61e65059

Added:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgIconsRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgIconsResult.java
Modified:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgResult.java
 /haikudepotserver-docs/src/main/latex/docs/part-api.tex
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgIconController.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgScreenshotController.java
 /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html
/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js /haikudepotserver-webapp/src/main/webapp/js/app/directive/pkgicondirective.js
 /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgiconservice.js
/haikudepotserver-webapp/src/main/webapp/js/app/service/pkgscreenshotservice.js
 /haikudepotserver-webapp/src/main/webapp/js/app/service/userstateservice.js
/haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/PkgApiIT.java

=======================================
--- /dev/null
+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgIconsRequest.java Mon Feb 24 10:10:13 2014 UTC
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.api1.model.pkg;
+
+public class GetPkgIconsRequest {
+
+    public String pkgName;
+
+    public GetPkgIconsRequest() {
+    }
+
+    public GetPkgIconsRequest(String pkgName) {
+        this.pkgName = pkgName;
+    }
+}
=======================================
--- /dev/null
+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgIconsResult.java Mon Feb 24 10:10:13 2014 UTC
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.api1.model.pkg;
+
+import java.util.List;
+
+public class GetPkgIconsResult {
+
+    public List<GetPkgIconsResult.PkgIcon> pkgIcons;
+
+    public static class PkgIcon {
+
+        public String mediaTypeCode;
+        public Integer size;
+
+    }
+
+}
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java Mon Feb 24 08:20:27 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java Mon Feb 24 10:10:13 2014 UTC
@@ -31,6 +31,13 @@

GetPkgResult getPkg(GetPkgRequest request) throws ObjectNotFoundException;

+    /**
+ * <p>Returns a list of meta-data regarding the icon data related to the pkg. This does not contain the icon
+     * data itself; just the meta data.</p>
+     */
+
+ GetPkgIconsResult getPkgIcons(GetPkgIconsRequest request) throws ObjectNotFoundException;
+
     /**
* <p>This request will configure the icons for the package nominated. Note that only certain configurations of * icon data may be acceptable; for example, it will require a 16x16px and 32x32px bitmap image.</p>
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgResult.java Sat Feb 8 10:16:22 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgResult.java Mon Feb 24 10:10:13 2014 UTC
@@ -22,12 +22,6 @@

     public Long modifyTimestamp;

-    /**
- * <p>This method will return true if the package has an icon configured for it.</p>
-     */
-
-    public Boolean hasIcon;
-
     public List<Version> versions;

     public static class Version {
=======================================
--- /haikudepotserver-docs/src/main/latex/docs/part-api.tex Mon Feb 24 08:20:27 2014 UTC +++ /haikudepotserver-docs/src/main/latex/docs/part-api.tex Mon Feb 24 10:10:13 2014 UTC
@@ -154,14 +154,14 @@

 \subsubsection{Get Package Icon}

-This API is able to provide the icon for a package. If there is no icon stored then this method will provide a fall-back image if the ``f'' query parameter is configured to ``true'' --- otherwise it will return a 404 HTTP status code. Providing a fallback image may not be possible in all cases. The request will return a {\tt Last-Modified} header. The timestamps here will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. The path includes a {\it mediatype-extension} which can have one of the following values; +This API is able to provide the icon for a package. If there is no icon stored then this method will provide a fall-back image if the ``f'' query parameter is configured to ``true'' --- otherwise it will return a 404 HTTP status code. Providing a fallback image may not be possible in all cases. The request will return a {\tt Last-Modified} header. The timestamps of this header will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. The path includes a {\it mediatype-extension} which can have one of the following values;

 \begin{itemize}
 \item png
 \item hvif
 \end{itemize}

-For example ``somepage.png''.
+Details of the API;

 \begin{itemize}
 \item HTTP Method : GET, HEAD
@@ -183,11 +183,11 @@

 An example URL is;

-\framebox{\tt http://localhost:8080/pkgicon/apr.png?size=32}
+\framebox{\tt http://localhost:8080/pkgicon/apr.png?size=32\&f=true}

 \subsubsection{Get Screenshot Image}

-This API is able to produce an image for a screenshot that can be identified by its code. The request will return a {\tt Last-Modified} header. The timestamps here will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. Requests for screenshot image should be accompanied by a target width and height. These values must be within a range of 1..1500. The image will maintain its aspect ratio as it is scaled. +This API is able to produce an image for a screenshot that can be identified by its code. The request will return a {\tt Last-Modified} header. The timestamps of this header will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. Requests for screenshot image should be accompanied by a target width and height. These values must be within a range of 1..1500. The image will maintain its aspect ratio as it is scaled to fit within the target width and height.

 \begin{itemize}
 \item HTTP Method : GET, HEAD
@@ -195,8 +195,8 @@
 \item Response Content-Type : image/png
 \item Query Parameters
   \begin{itemize}
- \item {\bf tw} : An integer that describes the width that the image should be scaled to - \item {\bf th} : An integer that describes the height that the image should be scaled to + \item {\bf tw} : An integer value that describes the width that the image should be scaled to + \item {\bf th} : An integer value that describes the height that the image should be scaled to
   \end{itemize}
 \item Expected HTTP Status Codes
   \begin{itemize}
@@ -217,26 +217,30 @@

 \begin{itemize}
 \item HTTP Method : GET
-\item Path : /pkgscreenshot/raw/$<$screenshotcode$>$
+\item Path : /pkgscreenshot/$<$screenshotcode$>$/raw
 \item Response Content-Type : {\it As per the stored data}
 \item Expected HTTP Status Codes
   \begin{itemize}
   \item {\bf 200} : The image data is provided in the response (for GET)
-  \item {\bf 404} : The screenshot was not found
+  \item {\bf 404} : The screenshot was not found for the code supplied
   \end{itemize}
 \end{itemize}

 An example URL is;

-\framebox{\tt http://localhost:8080/pkgscreenshot/raw/a78hw20fh2p20fh122jd92} +\framebox{\tt http://localhost:8080/pkgscreenshot/a78hw20fh2p20fh122jd92/raw}

 \subsubsection{Put Screenshot Image}

-This API is able to store an image for a screenshot for the nominated package. The screenshot will be ordered last. The payload of the PUT must be a PNG image. +This API is able to add an image as a screenshot for the nominated package. The screenshot will be ordered last. The payload of the PUT must be a PNG image that is a maximum of 1500x1500 pixels and 2MB in size.

 \begin{itemize}
-\item HTTP Method : PUT
-\item Path : /pkgscreenshot/$<$pkgname$>$.png
+\item HTTP Method : POST
+\item Path : /pkgscreenshot/$<$pkgname$>$/add
+\item Query Parameters
+  \begin{itemize}
+  \item {\bf format} : The string 'png' to define the image format.
+  \end{itemize}
 \item Expected HTTP Status Codes
   \begin{itemize}
   \item {\bf 200} : The screenshot image was stored
@@ -252,7 +256,7 @@

 An example URL is;

-\framebox{\tt http://localhost:8080/pkgscreenshot/apr.png}
+\framebox{\tt http://localhost:8080/pkgscreenshot/apr/add?format=png}



=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Mon Feb 24 08:20:27 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java Mon Feb 24 10:10:13 2014 UTC
@@ -53,6 +53,19 @@
     @Resource
     PkgService pkgService;

+ private Pkg getPkg(ObjectContext context, String pkgName) throws ObjectNotFoundException {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkState(!Strings.isNullOrEmpty(pkgName));
+
+        Optional<Pkg> pkgOptional = Pkg.getByName(context, pkgName);
+
+        if(!pkgOptional.isPresent()) {
+ throw new ObjectNotFoundException(Pkg.class.getSimpleName(), pkgName);
+        }
+
+        return pkgOptional.get();
+    }
+
     @Override
     public SearchPkgsResult searchPkgs(SearchPkgsRequest request) {
         Preconditions.checkNotNull(request);
@@ -209,15 +222,10 @@
         }

         GetPkgResult result = new GetPkgResult();
-        Optional<Pkg> pkgOptional = Pkg.getByName(context, request.name);
+        Pkg pkg = getPkg(context, request.name);

-        if(!pkgOptional.isPresent()) {
- throw new ObjectNotFoundException(Pkg.class.getSimpleName(), request.name);
-        }
-
-        result.name = pkgOptional.get().getName();
- result.modifyTimestamp = pkgOptional.get().getModifyTimestamp().getTime();
-        result.hasIcon = !pkgOptional.get().getPkgIcons().isEmpty();
+        result.name = pkg.getName();
+        result.modifyTimestamp = pkg.getModifyTimestamp().getTime();

         switch(request.versionType) {
             case LATEST:
@@ -227,7 +235,7 @@

Optional<PkgVersion> pkgVersionOptional = PkgVersion.getLatestForPkg(
                         context,
-                        pkgOptional.get(),
+                        pkg,
                         Lists.newArrayList(
                                 architectureOptional.get(),
Architecture.getByCode(context, Architecture.CODE_ANY).get(),
@@ -267,6 +275,31 @@
         }).isPresent();

     }
+
+    @Override
+ public GetPkgIconsResult getPkgIcons(GetPkgIconsRequest request) throws ObjectNotFoundException {
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!Strings.isNullOrEmpty(request.pkgName));
+
+        final ObjectContext context = serverRuntime.getContext();
+        Pkg pkg = getPkg(context, request.pkgName);
+
+        GetPkgIconsResult result = new GetPkgIconsResult();
+        result.pkgIcons = Lists.transform(
+                pkg.getPkgIcons(),
+                new Function<PkgIcon, GetPkgIconsResult.PkgIcon>() {
+                    @Override
+                    public GetPkgIconsResult.PkgIcon apply(PkgIcon input) {
+ GetPkgIconsResult.PkgIcon apiPkgIcon = new GetPkgIconsResult.PkgIcon();
+                        apiPkgIcon.size = input.getSize();
+ apiPkgIcon.mediaTypeCode = input.getMediaType().getCode();
+                        return apiPkgIcon;
+                    }
+                }
+        );
+
+        return result;
+    }

     @Override
public ConfigurePkgIconResult configurePkgIcon(ConfigurePkgIconRequest request) throws ObjectNotFoundException, BadPkgIconException {
@@ -274,16 +307,12 @@
         Preconditions.checkState(!Strings.isNullOrEmpty(request.pkgName));

         final ObjectContext context = serverRuntime.getContext();
- Optional<Pkg> pkgOptional = Pkg.getByName(context, request.pkgName);
-
-        if(!pkgOptional.isPresent()) {
- throw new ObjectNotFoundException(Pkg.class.getSimpleName(), request.pkgName);
-        }
+        Pkg pkg = getPkg(context, request.pkgName);

         User user = obtainAuthenticatedUser(context);

- if(!authorizationService.check(context, user, pkgOptional.get(), Permission.PKG_EDITICON)) { - logger.warn("attempt to configure the icon for package {}, but the user {} is not able to", pkgOptional.get().getName(), user.getNickname()); + if(!authorizationService.check(context, user, pkg, Permission.PKG_EDITICON)) { + logger.warn("attempt to configure the icon for package {}, but the user {} is not able to", pkg.getName(), user.getNickname());
             throw new AuthorizationFailureException();
         }

@@ -328,7 +357,7 @@
                                     mediaTypeOptional.get(),
                                     pkgIconApi.size,
                                     context,
-                                    pkgOptional.get()
+                                    pkg
                             )
                     );

@@ -347,7 +376,7 @@

// now we have some icons stored which may not be in the replacement data; we should remove those ones.

- for(PkgIcon pkgIcon : ImmutableList.copyOf(pkgOptional.get().getPkgIcons())) {
+        for(PkgIcon pkgIcon : ImmutableList.copyOf(pkg.getPkgIcons())) {
             if(!createdOrUpdatedPkgIcons.contains(pkgIcon)) {
                 context.deleteObjects(
                         pkgIcon.getPkgIconImage().get(),
@@ -357,14 +386,14 @@
             }
         }

-        pkgOptional.get().setModifyTimestamp();
+        pkg.setModifyTimestamp();

         context.commitChanges();

         logger.info(
                 "did configure icons for pkg {} (updated {}, removed {})",
                 new Object[] {
-                        pkgOptional.get().getName(),
+                        pkg.getName(),
                         updated,
                         removed
                 }
@@ -380,30 +409,26 @@
         Preconditions.checkState(!Strings.isNullOrEmpty(request.pkgName));

         final ObjectContext context = serverRuntime.getContext();
- Optional<Pkg> pkgOptional = Pkg.getByName(context, request.pkgName);
-
-        if(!pkgOptional.isPresent()) {
- throw new ObjectNotFoundException(Pkg.class.getSimpleName(), request.pkgName);
-        }
+        Pkg pkg = getPkg(context, request.pkgName);

         User user = obtainAuthenticatedUser(context);

- if(!authorizationService.check(context, user, pkgOptional.get(), Permission.PKG_EDITICON)) { - logger.warn("attempt to remove the icon for package {}, but the user {} is not able to", pkgOptional.get().getName(), user.getNickname()); + if(!authorizationService.check(context, user, pkg, Permission.PKG_EDITICON)) { + logger.warn("attempt to remove the icon for package {}, but the user {} is not able to", pkg.getName(), user.getNickname());
             throw new AuthorizationFailureException();
         }

- for(PkgIcon pkgIcon : ImmutableList.copyOf(pkgOptional.get().getPkgIcons())) {
+        for(PkgIcon pkgIcon : ImmutableList.copyOf(pkg.getPkgIcons())) {
             context.deleteObjects(
                     pkgIcon.getPkgIconImage().get(),
                     pkgIcon);
         }

-        pkgOptional.get().setModifyTimestamp();
+        pkg.setModifyTimestamp();

         context.commitChanges();

- logger.info("did remove icons for pkg {}",pkgOptional.get().getName());
+        logger.info("did remove icons for pkg {}",pkg.getName());

         return new RemovePkgIconResult();
     }
@@ -434,15 +459,11 @@
Preconditions.checkState(!Strings.isNullOrEmpty(getPkgScreenshotsRequest.pkgName));

         final ObjectContext context = serverRuntime.getContext();
- Optional<Pkg> pkgOptional = Pkg.getByName(context, getPkgScreenshotsRequest.pkgName);
-
-        if(!pkgOptional.isPresent()) {
- throw new ObjectNotFoundException(Pkg.class.getSimpleName(), getPkgScreenshotsRequest.pkgName);
-        }
+        Pkg pkg = getPkg(context, getPkgScreenshotsRequest.pkgName);

         GetPkgScreenshotsResult result = new GetPkgScreenshotsResult();
         result.items = Lists.transform(
-                pkgOptional.get().getSortedPkgScreenshots(),
+                pkg.getSortedPkgScreenshots(),
new Function<PkgScreenshot, GetPkgScreenshotsResult.PkgScreenshot>() {
                     @Override
public GetPkgScreenshotsResult.PkgScreenshot apply(PkgScreenshot pkgScreenshot) {
@@ -501,22 +522,18 @@
         Preconditions.checkNotNull(reorderPkgScreenshotsRequest.codes);

         final ObjectContext context = serverRuntime.getContext();
- Optional<Pkg> pkgOptional = Pkg.getByName(context, reorderPkgScreenshotsRequest.pkgName);
-
-        if(!pkgOptional.isPresent()) {
- throw new ObjectNotFoundException(Pkg.class.getSimpleName(), reorderPkgScreenshotsRequest.pkgName);
-        }
+        Pkg pkg = getPkg(context, reorderPkgScreenshotsRequest.pkgName);

         User authUser = obtainAuthenticatedUser(context);

- if(!authorizationService.check(context, authUser, pkgOptional.get(), Permission.PKG_EDITSCREENSHOT)) { + if(!authorizationService.check(context, authUser, pkg, Permission.PKG_EDITSCREENSHOT)) {
             throw new AuthorizationFailureException();
         }

- pkgOptional.get().reorderPkgScreenshots(reorderPkgScreenshotsRequest.codes);
+        pkg.reorderPkgScreenshots(reorderPkgScreenshotsRequest.codes);
         context.commitChanges();

- logger.info("did reorder the screenshots on package {}", pkgOptional.get().getName()); + logger.info("did reorder the screenshots on package {}", pkg.getName());

         return null;
     }
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgIconController.java Mon Feb 24 08:20:27 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgIconController.java Mon Feb 24 10:10:13 2014 UTC
@@ -163,10 +163,10 @@
@RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.HEAD)
     public void handleHead(
             HttpServletResponse response,
-            @RequestParam(value = KEY_SIZE) Integer size,
+            @RequestParam(value = KEY_SIZE, required = false) Integer size,
             @PathVariable(value = KEY_FORMAT) String format,
             @PathVariable(value = KEY_PKGNAME) String pkgName,
-            @RequestParam(value = KEY_FALLBACK) Boolean fallback)
+ @RequestParam(value = KEY_FALLBACK, required = false) Boolean fallback)
             throws IOException {
         handleHeadOrGet(
                 RequestMethod.HEAD,
@@ -180,10 +180,10 @@
@RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.GET)
     public void handleGet(
             HttpServletResponse response,
-            @RequestParam(value = KEY_SIZE, required = true) int size,
+            @RequestParam(value = KEY_SIZE, required = false) Integer size,
             @PathVariable(value = KEY_FORMAT) String format,
             @PathVariable(value = KEY_PKGNAME) String pkgName,
-            @RequestParam(value = KEY_FALLBACK) Boolean fallback)
+ @RequestParam(value = KEY_FALLBACK, required = false) Boolean fallback)
     throws IOException {
         handleHeadOrGet(
                 RequestMethod.GET,
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgScreenshotController.java Tue Feb 18 10:16:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/controller/PkgScreenshotController.java Mon Feb 24 10:10:13 2014 UTC
@@ -59,31 +59,22 @@
     @Resource
     AuthorizationService authorizationService;

- @RequestMapping(value = "/{"+KEY_SCREENSHOTCODE+"}.{"+KEY_FORMAT+"}", method = RequestMethod.HEAD)
-    public void fetchHead(
-            HttpServletRequest request,
+    private void handleHeadOrGet(
+            RequestMethod requestMethod,
             HttpServletResponse response,
-            @RequestParam(value = KEY_TARGETWIDTH) Integer targetWidth,
-            @RequestParam(value = KEY_TARGETHEIGHT) Integer targetHeight,
-            @PathVariable(value = KEY_FORMAT) String format,
- @PathVariable(value = KEY_SCREENSHOTCODE) String screenshotCode)
+            Integer targetWidth,
+            Integer targetHeight,
+            String format,
+            String screenshotCode)
             throws IOException {

- if(null!=targetWidth && (targetWidth <= 0 || targetWidth > SCREENSHOT_SIDE_LIMIT)) {
+        if(targetWidth <= 0 || targetWidth > SCREENSHOT_SIDE_LIMIT) {
             throw new BadSize();
         }

- if(null!=targetHeight && (targetHeight <= 0 || targetHeight > SCREENSHOT_SIDE_LIMIT)) {
+        if(targetHeight <= 0 || targetHeight > SCREENSHOT_SIDE_LIMIT) {
             throw new BadSize();
         }
-
-        if(null==targetHeight) {
-            targetHeight = SCREENSHOT_SIDE_LIMIT * 2;
-        }
-
-        if(null==targetWidth) {
-            targetWidth = SCREENSHOT_SIDE_LIMIT * 2;
-        }

         if(Strings.isNullOrEmpty(screenshotCode)) {
             throw new MissingScreenshotCode();
@@ -100,26 +91,64 @@
             throw new ScreenshotNotFound();
         }

- ByteCounterOutputStream byteCounter = new ByteCounterOutputStream(new NoOpOutputStream());
+        response.setContentType(MediaType.PNG.toString());
+        response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600");

-        pkgService.writePkgScreenshotImage(
-                byteCounter,
-                context,
-                screenshotOptional.get(),
-                targetWidth,
-                targetHeight);
-
- response.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(byteCounter.getCounter()));
-        response.setContentType(MediaType.PNG.toString());
         response.setDateHeader(
                 HttpHeaders.LAST_MODIFIED,
screenshotOptional.get().getPkg().getModifyTimestampSecondAccuracy().getTime());

+        switch(requestMethod) {
+            case HEAD:
+ ByteCounterOutputStream byteCounter = new ByteCounterOutputStream(new NoOpOutputStream());
+
+                pkgService.writePkgScreenshotImage(
+                        byteCounter,
+                        context,
+                        screenshotOptional.get(),
+                        targetWidth,
+                        targetHeight);
+
+ response.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(byteCounter.getCounter()));
+
+                break;
+
+            case GET:
+                pkgService.writePkgScreenshotImage(
+                        response.getOutputStream(),
+                        context,
+                        screenshotOptional.get(),
+                        targetWidth,
+                        targetHeight);
+                break;
+
+            default:
+ throw new IllegalStateException("unhandled request method; "+requestMethod);
+        }
+
+    }
+
+ @RequestMapping(value = "/{"+KEY_SCREENSHOTCODE+"}.{"+KEY_FORMAT+"}", method = RequestMethod.HEAD)
+    public void handleHead(
+            HttpServletResponse response,
+            @RequestParam(value = KEY_TARGETWIDTH) Integer targetWidth,
+            @RequestParam(value = KEY_TARGETHEIGHT) Integer targetHeight,
+            @PathVariable(value = KEY_FORMAT) String format,
+ @PathVariable(value = KEY_SCREENSHOTCODE) String screenshotCode)
+            throws IOException {
+
+        handleHeadOrGet(
+                RequestMethod.HEAD,
+                response,
+                targetWidth,
+                targetHeight,
+                format,
+                screenshotCode);
+
     }

@RequestMapping(value = "/{"+KEY_SCREENSHOTCODE+"}.{"+KEY_FORMAT+"}", method = RequestMethod.GET)
-    public void fetchGet(
-            HttpServletRequest request,
+    public void handleGet(
             HttpServletResponse response,
@RequestParam(value = KEY_TARGETWIDTH, required = true) int targetWidth, @RequestParam(value = KEY_TARGETHEIGHT, required = true) int targetHeight,
@@ -127,50 +156,22 @@
@PathVariable(value = KEY_SCREENSHOTCODE) String screenshotCode)
             throws IOException {

-        if(targetWidth <= 0 || targetWidth > SCREENSHOT_SIDE_LIMIT) {
-            throw new BadSize();
-        }
-
-        if(targetHeight <= 0 || targetHeight > SCREENSHOT_SIDE_LIMIT) {
-            throw new BadSize();
-        }
-
-        if(Strings.isNullOrEmpty(screenshotCode)) {
-            throw new MissingScreenshotCode();
-        }
-
-        if(Strings.isNullOrEmpty(format) || !"png".equals(format)) {
-            throw new MissingOrBadFormat();
-        }
-
-        ObjectContext context = serverRuntime.getContext();
- Optional<PkgScreenshot> screenshotOptional = PkgScreenshot.getByCode(context, screenshotCode);
-
-        if(!screenshotOptional.isPresent()) {
-            throw new ScreenshotNotFound();
-        }
-
-        response.setContentType(MediaType.PNG.toString());
-        response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600");
-        response.setDateHeader(
-                HttpHeaders.LAST_MODIFIED,
- screenshotOptional.get().getPkg().getModifyTimestampSecondAccuracy().getTime());
-
-        pkgService.writePkgScreenshotImage(
-                response.getOutputStream(),
-                context,
-                screenshotOptional.get(),
+        handleHeadOrGet(
+                RequestMethod.GET,
+                response,
                 targetWidth,
-                targetHeight);
+                targetHeight,
+                format,
+                screenshotCode);
+
     }

     /**
* <p>This one downloads the raw data that is stored for a screenshot.</p>
      */

- @RequestMapping(value = "/raw/{"+KEY_SCREENSHOTCODE+"}", method = RequestMethod.GET)
-    public void fetchRawGet(
-            HttpServletRequest request,
+ @RequestMapping(value = "/{"+KEY_SCREENSHOTCODE+"}/raw", method = RequestMethod.GET)
+    public void handleRawGet(
             HttpServletResponse response,
@PathVariable(value = KEY_SCREENSHOTCODE) String screenshotCode)
             throws IOException {
@@ -217,11 +218,11 @@
* <p>This handler will take-up an HTTP PUT that provides a new screenshot for the package.</p>
      */

- @RequestMapping(value = "/{"+KEY_PKGNAME+"}.{"+KEY_FORMAT+"}", method = RequestMethod.PUT)
-    public void put(
+ @RequestMapping(value = "/{"+KEY_PKGNAME+"}/add", method = RequestMethod.POST)
+    public void handleAdd(
             HttpServletRequest request,
             HttpServletResponse response,
-            @PathVariable(value = KEY_FORMAT) String format,
+ @RequestParam(value = KEY_FORMAT, required = true) String format, @PathVariable(value = KEY_PKGNAME) String pkgName) throws IOException {

if(Strings.isNullOrEmpty(pkgName) | | !Pkg.NAME_PATTERN.matcher(pkgName).matches()) {
@@ -267,7 +268,7 @@

         context.commitChanges();

-        response.setHeader(HEADER_SCREENSHOTCODE,screenshotCode);
+        response.setHeader(HEADER_SCREENSHOTCODE, screenshotCode);
         response.setStatus(HttpServletResponse.SC_OK);
     }

=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Tue Feb 18 00:02:17 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Mon Feb 24 10:10:13 2014 UTC
@@ -63,6 +63,9 @@
<li ng-show="canEditIcon()" pkg="pkg" show-if-pkg-permission="'PKG_EDITICON'">
                     <a href="" ng-click="goEditIcon()">Edit icons</a>
                 </li>
+                <li ng-show="pkgIconHvifUrl">
+ <a href="{{pkgIconHvifUrl}}">Download icon in 'hvif' format</a>
+                </li>
<li ng-show="canEditScreenshots()" pkg="pkg" show-if-pkg-permission="'PKG_EDITSCREENSHOT'"> <a href="" ng-click="goEditScreenshots()">Edit screenshots</a>
                 </li>
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Mon Feb 24 08:20:27 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Mon Feb 24 10:10:13 2014 UTC
@@ -8,11 +8,11 @@
     [
         '$scope','$log','$location','$routeParams',
         'jsonRpc','constants','userState','errorHandling',
-        'pkgScreenshot',
+        'pkgScreenshot','pkgIcon',
         function(
             $scope,$log,$location,$routeParams,
             jsonRpc,constants,userState,errorHandling,
-            pkgScreenshot) {
+            pkgScreenshot, pkgIcon) {

             var SCREENSHOT_THUMBNAIL_TARGETWIDTH = 480;
             var SCREENSHOT_THUMBNAIL_TARGETHEIGHT = 320;
@@ -21,6 +21,9 @@
             $scope.pkg = undefined;
             $scope.pkgScreenshots = undefined;
             $scope.pkgScreenshotOffset = 0;
+            $scope.pkgIconHvifUrl = undefined;
+
+            var hasPkgIcons = undefined;

             refetchPkg();

@@ -29,7 +32,7 @@
             }

             $scope.canRemoveIcon = function() {
-                return $scope.pkg && $scope.pkg.hasIcon;
+                return $scope.pkg && hasPkgIcons;
             }

             $scope.canEditIcon = function() {
@@ -79,12 +82,41 @@
                         $log.info('found '+result.name+' pkg');
                         refreshBreadcrumbItems();
                         refetchPkgScreenshots();
+                        refetchPkgIconMetaData();
                     },
                     function(err) {
                         errorHandling.handleJsonRpcError(err);
                     }
                 );
             }
+
+            function refetchPkgIconMetaData() {
+
+                $scope.pkgIconHvifUrl = undefined;
+
+                jsonRpc.call(
+                        constants.ENDPOINT_API_V1_PKG,
+                        "getPkgIcons",
+                        [{ pkgName: $routeParams.name }]
+                    ).then(
+                    function(result) {
+
+                        var has = !!_.findWhere(
+                            result.pkgIcons,
+ { mediaTypeCode : constants.MEDIATYPE_HAIKUVECTORICONFILE });
+
+                        $scope.pkgIconHvifUrl = has ? pkgIcon.url(
+                            $scope.pkg,
+ constants.MEDIATYPE_HAIKUVECTORICONFILE) : undefined;
+
+                        hasPkgIcons = !!result.pkgIcons.length;
+                    },
+                    function(err) {
+                        errorHandling.handleJsonRpcError(err);
+                    }
+                );
+
+            }

             // ------------------------
             // SCREENSHOTS
@@ -162,7 +194,7 @@
                     ).then(
                     function(result) {
$log.info('removed icons for '+$routeParams.name+' pkg');
-                        $scope.pkg.hasIcon = false;
+                        refetchPkgIconMetaData();
                         $scope.pkg.modifyTimestamp = new Date().getTime();
                     },
                     function(err) {
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/directive/pkgicondirective.js Mon Jan 13 10:50:43 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/pkgicondirective.js Mon Feb 24 10:10:13 2014 UTC
@@ -19,8 +19,8 @@
                 pkg:'='
             },
             controller:
-                ['$scope','pkgIcon',
-                    function($scope,pkgIcon) {
+                ['$scope','pkgIcon','constants',
+                    function($scope,pkgIcon,constants) {

                         $scope.imgUrl = '';

@@ -30,7 +30,7 @@
throw 'pkg does not contain a name to identify the pkg for the pkg-icon';
                                 }
                                 else {
- $scope.imgUrl = pkgIcon.url($scope.pkg, $scope.size); + $scope.imgUrl = pkgIcon.url($scope.pkg, constants.MEDIATYPE_PNG, $scope.size);
                                 }
                             }
                         }
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgiconservice.js Mon Feb 24 08:20:27 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgiconservice.js Mon Feb 24 10:10:13 2014 UTC
@@ -14,169 +14,43 @@

             var PkgIcon = {

- // these are errors that may be returned to the caller below. They match to the HTTP status codes
-                // used, but this should not be relied upon.
-
-                errorCodes : {
-                    BADFORMATORSIZEERROR : 415,
-                    NOTFOUND : 404,
-                    BADREQUEST : 400,
-                    UNKNOWN : -1
-                },
-
                 /**
- * <p>This is a map of HTTP headers that is sent on each request into the server.</p> + * <p>This function will provide a URL to the packages' icon.</p>
                  */

-                headers : {},
-
-                /**
- * <p>This method will set the HTTP header that is sent on each request. This is handy,
-                 * for example for authentication.</p>
-                 */
-
-                setHeader : function(name, value) {
-
-                    if(!name || 0==''+name.length) {
-                        throw 'the name of the http header is required';
-                    }
-
-                    if(!value || 0==''+value.length) {
-                        delete PkgIcon.headers[name];
-                    }
-                    else {
-                        PkgIcon.headers[name] = value;
-                    }
-
-                },
-
- // this function will set the icon for the package. Note that there may be more than one variant and - // this method will need to be invoked for each variant; or example 16px or 32px versions of the same - // package icon. It is also possible to store a vector (hvif) file of the icon as well.
-
- setPkgIcon : function(pkg, mediaType, iconFile, expectedSize) {
-
- // this function will check to see if the string 's' ends with the string 'e'.
-
-                    function endsWith(s, e) {
-                        return -1 != s.indexOf(e, s.length - e.length);
-                    }
-
-                    function mediaTypeToExtension(m) {
-                        switch(m) {
-                            case constants.MEDIATYPE_HAIKUVECTORICONFILE:
-                                return 'hvif';
-
-                            case constants.MEDIATYPE_PNG:
-                                return 'png';
-
-                            default:
-                                throw 'unknown media type; '+m;
-                        }
-                    }
+                url : function(pkg, mediaTypeCode, size) {

                     if(!pkg) {
- throw 'the pkg must be supplied to set the pkg icon'; + throw 'the pkg must be supplied to get the package icon url';
                     }

-                    if(!iconFile) {
- throw 'to set the pkg icon for '+pkg.name+' the image file must be provided';
-                    }
+                    var u = '/pkgicon/' + pkg.name;

- // if there is no media type supplied then try to derive on from the file extension.
-
-                    if(!mediaType || !mediaType.length) {
-                        if(endsWith(iconFile.name,'.png')) {
-                            mediaType = constants.MEDIATYPE_PNG;
-                        }
-
-                        if(endsWith(iconFile.name,'.hvif')) {
- mediaType = constants.MEDIATYPE_HAIKUVECTORICONFILE;
-                        }
-
-                        if(!mediaType) {
- throw 'unable to derive a media type from the file name; ' + iconFile.name;
-                        }
+                    if(!mediaTypeCode) {
+ throw 'the media type code is required to get the package icon url';
                     }

-                    var isBitmap = mediaType == constants.MEDIATYPE_PNG;
+                    switch(mediaTypeCode) {
+                        case constants.MEDIATYPE_HAIKUVECTORICONFILE:
+                            u += '.hvif';
+                            break;

-                    if(isBitmap) {
- if(!expectedSize || !(16==expectedSize || 32==expectedSize)) {
-                            throw 'the expected size must be 16 or 32px';
-                        }
-                    }
-                    else {
-                        if(expectedSize) {
- throw 'the expected size cannot be supplied because the image is vector';
-                        }
-                    }
-
-                    var deferred = $q.defer();
-
- var path = '/pkgicon/'+pkg.name+'.'+mediaTypeToExtension(mediaType);
-
-                    if(expectedSize) {
-                        path += '?s=' + expectedSize;
-                    }
-
-                    $http({
-                        cache: false,
-                        method: 'PUT',
-                        url: path,
-                        headers: _.extend(
-                            { 'Content-Type' : mediaType },
-                            PkgIcon.headers),
-                        data: iconFile
-                    })
-                        .success(function(data,status,header,config) {
-                            deferred.resolve();
-                        })
-                        .error(function(data,status,header,config) {
-                            switch(status) {
-                                case 200:
-                                    deferred.resolve();
-                                    break;
-
-                                case 415: // unsupported media type
- deferred.reject(PkgIcon.errorCodes.BADFORMATORSIZEERROR);
-                                    break;
-
-                                case 400: // bad request
- deferred.reject(PkgIcon.errorCodes.BADREQUEST);
-                                    break;
-
-                                case 404: // not found
- deferred.reject(PkgIcon.errorCodes.NOTFOUND);
-                                    break;
-
-                                default:
- deferred.reject(PkgIcon.errorCodes.UNKNOWN);
-                                    break;
-
+                        case constants.MEDIATYPE_PNG: {
+                            if(!size || !(32==size||16==size)) {
+ throw 'the size is not valid for obtaining the package icon url';
                             }
-                        });

-                    return deferred.promise;
-                },
+                            u += '.png?f=true&s=' + size
+                        }
+                            break;

-                /**
- * <p>This function will provide a URL to the packages' icon.</p>
-                 */
-
-                url : function(pkg, size) {
-                    if(!pkg) {
- throw 'the pkg must be supplied to get the package icon url';
+                        default:
+                            throw 'unknown media type; ' + mediaTypeCode;
                     }

-                    if(!size || !(32==size||16==size)) {
- throw 'the size is not valid for obtaining the package icon url';
-                    }
-
- var u = '/pkgicon/' + pkg.name + '.png?s=' + size + '&f=true';
-
                     if(pkg.modifyTimestamp) {
-                        u += '&m=' + pkg.modifyTimestamp;
+                        u += -1==u.indexOf('?') ? '?' : '&';
+                        u += 'm=' + pkg.modifyTimestamp;
                     }

                     return u;
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgscreenshotservice.js Tue Feb 18 00:02:17 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgscreenshotservice.js Mon Feb 24 10:10:13 2014 UTC
@@ -66,8 +66,8 @@

                     $http({
                         cache: false,
-                        method: 'PUT',
-                        url: '/pkgscreenshot/'+pkg.name+'.png',
+                        method: 'POST',
+                        url: '/pkgscreenshot/'+pkg.name+'/add?format=png',
                         headers: _.extend(
                             { 'Content-Type' : 'image/png' },
                             PkgScreenshot.headers),
@@ -123,7 +123,7 @@
throw 'the code must be supplied to derive a url for the screenshot image';
                     }

-                    return '/pkgscreenshot/raw/' + code;
+                    return '/pkgscreenshot/' + code + '/raw';
                 },

                 /**
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/service/userstateservice.js Tue Feb 18 00:02:17 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/service/userstateservice.js Mon Feb 24 10:10:13 2014 UTC
@@ -210,7 +210,6 @@

// remove the Authorization header for HTTP transport
                             jsonRpc.setHeader('Authorization');
-                            pkgIcon.setHeader('Authorization');
                             pkgScreenshot.setHeader('Authorization');
                         }
                         else {
@@ -226,7 +225,6 @@
var basic = 'Basic '+window.btoa(''+value.nickname+':'+value.passwordClear);

                             jsonRpc.setHeader('Authorization',basic);
-                            pkgIcon.setHeader('Authorization',basic);
                             pkgScreenshot.setHeader('Authorization',basic);

                             user = value;
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/PkgApiIT.java Mon Feb 24 08:20:27 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotsever/api1/PkgApiIT.java Mon Feb 24 10:10:13 2014 UTC
@@ -76,7 +76,6 @@
         GetPkgResult result = pkgApi.getPkg(request);
         // ------------------------------------

- Assertions.assertThat(result.hasIcon).isTrue(); // icons are preloaded in the test data
         Assertions.assertThat(result.name).isEqualTo("pkg1");
         Assertions.assertThat(result.versions.size()).isEqualTo(1);
Assertions.assertThat(result.versions.get(0).architectureCode).isEqualTo("x86");
@@ -111,9 +110,23 @@
         }
     }

-    /**
-     * <p>Here we are trying to load the HVIF data in as PNG images.</p>
-     */
+    @Test
+    public void testGetPkgIcons() throws Exception {
+
+ IntegrationTestSupportService.StandardTestData data = integrationTestSupportService.createStandardTestData();
+
+        // ------------------------------------
+ GetPkgIconsResult result = pkgApi.getPkgIcons(new GetPkgIconsRequest("pkg1"));
+        // ------------------------------------
+
+        Assertions.assertThat(result.pkgIcons.size()).isEqualTo(2);
+        // check more stuff...
+
+    }
+
+        /**
+ * <p>Here we are trying to load the HVIF data in as PNG images.</p>
+         */

     @Test
     public void testConfigurePkgIcon_badData() throws Exception {

==============================================================================
Revision: b8dc530bc215
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Mon Feb 24 10:23:41 2014 UTC
Log:      + documentation updates

http://code.google.com/p/haiku-depot-web-app/source/detail?r=b8dc530bc215

Modified:
 /haikudepotserver-docs/src/main/latex/docs/part-api.tex

=======================================
--- /haikudepotserver-docs/src/main/latex/docs/part-api.tex Mon Feb 24 10:10:13 2014 UTC +++ /haikudepotserver-docs/src/main/latex/docs/part-api.tex Mon Feb 24 10:23:41 2014 UTC
@@ -154,7 +154,7 @@

 \subsubsection{Get Package Icon}

-This API is able to provide the icon for a package. If there is no icon stored then this method will provide a fall-back image if the ``f'' query parameter is configured to ``true'' --- otherwise it will return a 404 HTTP status code. Providing a fallback image may not be possible in all cases. The request will return a {\tt Last-Modified} header. The timestamps of this header will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. The path includes a {\it mediatype-extension} which can have one of the following values; +This API is able to provide the icon for a package. If there is no icon stored then this method will provide a fall-back image if the {\t f} query parameter is configured to ``true'' --- otherwise it will return a 404 HTTP status code. Providing a fallback image may not be possible in all cases. The request will return a {\tt Last-Modified} header. The timestamps of this header will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. The path includes a {\it mediatype-extension} which can have one of the following values;

 \begin{itemize}
 \item png
@@ -181,13 +181,18 @@
   \end{itemize}
 \end{itemize}

-An example URL is;
+An example URL for obtaining a bitmap image;

 \framebox{\tt http://localhost:8080/pkgicon/apr.png?size=32\&f=true}
+
+An example URL for obtaining a Haiku vector image file image;
+
+\framebox{\tt http://localhost:8080/pkgicon/apr.hvif}
+

 \subsubsection{Get Screenshot Image}

-This API is able to produce an image for a screenshot that can be identified by its code. The request will return a {\tt Last-Modified} header. The timestamps of this header will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. Requests for screenshot image should be accompanied by a target width and height. These values must be within a range of 1..1500. The image will maintain its aspect ratio as it is scaled to fit within the target width and height. +This API is able to produce an image for a screenshot. The screenshot is identified in the path by its code. The response will return a {\tt Last-Modified} header. The timestamps of this header will correlate to the {\it modifyTimestamp} that is provided in API responses such as {\tt GetPkResult} and {\tt SearchPkgsResult}. The value for {\it modifyTimestamp} will be at millisecond resolution, but the HTTP headers will be at second resolution. Requests for screenshot image should be accompanied by a target width and height. These values must be within a range of 1..1500. The image will maintain its aspect ratio as it is scaled to fit within the supplied target width and height.

 \begin{itemize}
 \item HTTP Method : GET, HEAD
@@ -230,9 +235,9 @@

\framebox{\tt http://localhost:8080/pkgscreenshot/a78hw20fh2p20fh122jd92/raw}

-\subsubsection{Put Screenshot Image}
+\subsubsection{Add Screenshot Image}

-This API is able to add an image as a screenshot for the nominated package. The screenshot will be ordered last. The payload of the PUT must be a PNG image that is a maximum of 1500x1500 pixels and 2MB in size. +This API is able to add an image as a screenshot for the nominated package. The screenshot will be ordered last. The payload of the POST must be a PNG image that is a maximum of 1500x1500 pixels and a maximum of 2MB in size.

 \begin{itemize}
 \item HTTP Method : POST

Other related posts:

  • » [haiku-depot-web] [haiku-depot-web-app] 3 new revisions pushed by haiku.li...@xxxxxxxxx on 2014-02-24 10:23 GMT - haiku-depot-web-app