[haiku-depot-web] [haiku-depot-web-app] 4 new revisions pushed by haiku.li...@xxxxxxxxx on 2014-08-31 19:38 GMT

  • From: haiku-depot-web-app@xxxxxxxxxxxxxx
  • To: haiku-depot-web@xxxxxxxxxxxxx
  • Date: Sun, 31 Aug 2014 19:39:08 +0000

master moved from 92a0d41c5056 to 6dcf4d936436

4 new revisions:

Revision: 5087ee187803
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sat Aug 30 10:29:45 2014 UTC
Log:      simple / limited user interface for simple browsers
https://code.google.com/p/haiku-depot-web-app/source/detail?r=5087ee187803

Revision: 8dbcbd8a7f4b
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sun Aug 31 09:07:52 2014 UTC
Log: sort out problems with pagination links in java as well as javascript
https://code.google.com/p/haiku-depot-web-app/source/detail?r=8dbcbd8a7f4b

Revision: ccc92b6dfc59
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sun Aug 31 10:18:17 2014 UTC
Log:      ability to get localized names back for some reference data...
https://code.google.com/p/haiku-depot-web-app/source/detail?r=ccc92b6dfc59

Revision: 6dcf4d936436
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sun Aug 31 12:05:02 2014 UTC
Log:      updates to integration tests for lower case pkg category names...
https://code.google.com/p/haiku-depot-web-app/source/detail?r=6dcf4d936436

==============================================================================
Revision: 5087ee187803
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sat Aug 30 10:29:45 2014 UTC
Log:      simple / limited user interface for simple browsers

https://code.google.com/p/haiku-depot-web-app/source/detail?r=5087ee187803

Added:
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageConstants.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageHelper.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageLocaleResolver.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageObjectNotFoundException.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/controller/HomeController.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/controller/ViewPkgController.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/NaturalLanguageChooserTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PaginationLinksTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PkgIconTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PkgVersionLabelTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PkgVersionLinkTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PlainTextContentTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/RatingIndicatorTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/TimestampTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/model/Pagination.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/package-info.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/singlepage/controller/EntryPointController.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/singlepage/package-info.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/RobotController.java
 /haikudepotserver-webapp/src/main/webapp/WEB-INF/includes/favicons.jsp
 /haikudepotserver-webapp/src/main/webapp/WEB-INF/multipage.tld
 /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/home.jsp
/haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/includes/banner.jsp /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/includes/prelude.jsp /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/singlepage/launch.jsp
 /haikudepotserver-webapp/src/main/webapp/css/multipage/banner.css
/haikudepotserver-webapp/src/main/webapp/css/singlepage/addedituserrating.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/banner.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/breadcrumbs.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/createuser.css
/haikudepotserver-webapp/src/main/webapp/css/singlepage/editpkgscreenshots.css /haikudepotserver-webapp/src/main/webapp/css/singlepage/editpkgversionlocalization.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/home.css
/haikudepotserver-webapp/src/main/webapp/css/singlepage/listauthorizationpkgrules.css /haikudepotserver-webapp/src/main/webapp/css/singlepage/listrepositories.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/listusers.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/main.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/pkgfeedbuilder.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/unsupported.css
 /haikudepotserver-webapp/src/main/webapp/css/singlepage/viewpkg.css
 /haikudepotserver-webapp/src/main/webapp/img/paginationleft.png
 /haikudepotserver-webapp/src/main/webapp/img/paginationleft.svg
 /haikudepotserver-webapp/src/main/webapp/img/paginationright.png
 /haikudepotserver-webapp/src/main/webapp/img/paginationright.svg
 /haikudepotserver-webapp/src/main/webapp/img/starhalf.png
 /haikudepotserver-webapp/src/main/webapp/img/staroff.png
 /haikudepotserver-webapp/src/main/webapp/img/staron.png
/haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java
Deleted:
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/web/controller/EntryPointController.java
 /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/entryPoint.jsp
 /haikudepotserver-webapp/src/main/webapp/css/addedituserrating.css
 /haikudepotserver-webapp/src/main/webapp/css/banner.css
 /haikudepotserver-webapp/src/main/webapp/css/breadcrumbs.css
 /haikudepotserver-webapp/src/main/webapp/css/createuser.css
 /haikudepotserver-webapp/src/main/webapp/css/editpkgscreenshots.css
 /haikudepotserver-webapp/src/main/webapp/css/editpkgversionlocalization.css
 /haikudepotserver-webapp/src/main/webapp/css/haikudepotserver.css
 /haikudepotserver-webapp/src/main/webapp/css/home.css
 /haikudepotserver-webapp/src/main/webapp/css/listauthorizationpkgrules.css
 /haikudepotserver-webapp/src/main/webapp/css/listrepositories.css
 /haikudepotserver-webapp/src/main/webapp/css/listusers.css
 /haikudepotserver-webapp/src/main/webapp/css/pkgfeedbuilder.css
 /haikudepotserver-webapp/src/main/webapp/css/unsupported.css
 /haikudepotserver-webapp/src/main/webapp/css/viewpkg.css
/haikudepotserver-webapp/src/main/webapp/js/app/directive/paginationarrowdirective.js
Modified:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ObjectNotFoundException.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/Architecture.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/NaturalLanguage.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgCategory.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java
 /haikudepotserver-webapp/src/main/resources/jawr.properties
 /haikudepotserver-webapp/src/main/resources/messages.properties
 /haikudepotserver-webapp/src/main/resources/messages_de.properties
 /haikudepotserver-webapp/src/main/resources/spring/servlet-context.xml
 /haikudepotserver-webapp/src/main/webapp/WEB-INF/includes/unsupported.jsp
 /haikudepotserver-webapp/src/main/webapp/js/app/constants.js
 /haikudepotserver-webapp/src/main/webapp/js/app/controller/about.html
/haikudepotserver-webapp/src/main/webapp/js/app/directive/paginationcontroldirective.js /haikudepotserver-webapp/src/main/webapp/js/app/directive/ratingindicatordirective.js /haikudepotserver-webapp/src/main/webapp/js/app/service/breadcrumbfactoryservice.js
 /haikudepotserver-webapp/src/main/webapp/js/app/service/pkgservice.js

=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageConstants.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,14 @@
+package org.haikuos.haikudepotserver.multipage;
+
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+public class MultipageConstants {
+
+    public final static String PATH_MULTIPAGE = "/multipage";
+
+    public final static String KEY_NATURALLANGUAGECODE = "natlangcode";
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageHelper.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import org.apache.cayenne.ObjectContext;
+import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * <p>Helper (static) methods for the multipage part of the system.</p>
+ */
+
+public class MultipageHelper {
+
+    /**
+ * <p>This will look at parameters on the supplied request and will return a natural language. It will + * resort to English language if no other language is able to be derived.</p>
+     */
+
+ public static NaturalLanguage deriveNaturalLanguage(ObjectContext context, HttpServletRequest request) {
+        Preconditions.checkNotNull(context);
+
+        if(null!=request) {
+ String naturalLanguageCode = request.getParameter(MultipageConstants.KEY_NATURALLANGUAGECODE);
+
+            if(!Strings.isNullOrEmpty(naturalLanguageCode)) {
+ Optional<NaturalLanguage> naturalLanguageOptional = NaturalLanguage.getByCode(context, naturalLanguageCode);
+
+                if(!naturalLanguageOptional.isPresent()) {
+ throw new IllegalStateException("the natural language for code " + naturalLanguageCode + " was not able to be found");
+                }
+
+                return naturalLanguageOptional.get();
+            }
+
+            // see if we can deduce it from the locale.
+
+            Locale locale = request.getLocale();
+
+            if(null != locale) {
+ Iterator<String> langI = Splitter.on(Pattern.compile("[-_]")).split(locale.toLanguageTag()).iterator();
+
+                if(langI.hasNext()) {
+ Optional<NaturalLanguage> naturalLanguageOptional = NaturalLanguage.getByCode(context, langI.next());
+
+                    if(naturalLanguageOptional.isPresent()) {
+                        return naturalLanguageOptional.get();
+                    }
+                }
+
+            }
+        }
+
+ return NaturalLanguage.getByCode(context, NaturalLanguage.CODE_ENGLISH).get();
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageLocaleResolver.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage;
+
+import org.apache.cayenne.configuration.server.ServerRuntime;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Locale;
+
+public class MultipageLocaleResolver implements org.springframework.web.servlet.LocaleResolver {
+
+    @Resource
+    ServerRuntime serverRuntime;
+
+    @Override
+    public Locale resolveLocale(HttpServletRequest request) {
+ return MultipageHelper.deriveNaturalLanguage(serverRuntime.getContext(), request).toLocale();
+    }
+
+    @Override
+ public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
+        // ignore.
+    }
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/MultipageObjectNotFoundException.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+/**
+ * <p>This exception will return a 404 when it arises.</p>
+ */
+
+@ResponseStatus(HttpStatus.NOT_FOUND)
+public class MultipageObjectNotFoundException extends Exception {
+
+    public String entityName;
+    public Object identifier;
+
+ public MultipageObjectNotFoundException(String entityName, Object identifier) {
+        super();
+
+        if(null==entityName || 0==entityName.length()) {
+            throw new IllegalStateException("the entity name is required");
+        }
+
+        if(null==identifier) {
+            throw new IllegalStateException("the identifier is required");
+        }
+
+        this.entityName = entityName;
+        this.identifier = identifier;
+    }
+
+    public String getEntityName() {
+        return entityName;
+    }
+
+    public Object getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public String getMessage() {
+ return String.format("the entity %s was not able to be found with the identifier %s",getEntityName(),getIdentifier().toString());
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/controller/HomeController.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.controller;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.haikuos.haikudepotserver.dataobjects.Architecture;
+import org.haikuos.haikudepotserver.dataobjects.PkgCategory;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
+import org.haikuos.haikudepotserver.multipage.MultipageConstants;
+import org.haikuos.haikudepotserver.multipage.MultipageHelper;
+import org.haikuos.haikudepotserver.multipage.model.Pagination;
+import org.haikuos.haikudepotserver.pkg.PkgOrchestrationService;
+import org.haikuos.haikudepotserver.pkg.model.PkgSearchSpecification;
+import org.haikuos.haikudepotserver.support.AbstractSearchSpecification;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * <p>Renders the home page of the multi-page (simple) view of the application.</p>
+ */
+
+@Controller
+@RequestMapping(MultipageConstants.PATH_MULTIPAGE)
+public class HomeController {
+
+    /**
+     * <p>This defines the type of display of packages that are shown.</p>
+     */
+
+    public enum ViewCriteriaType {
+        FEATURED,
+        ALL,
+        CATEGORIES,
+        MOSTRECENT,
+        MOSTVIEWED;
+
+        public String getTitleKey() {
+            return "home.viewCriteriaType." + name().toLowerCase();
+        }
+
+    }
+
+    // these should correspond to the single-page keys for the home page.
+    public final static String KEY_OFFSET = "o";
+    public final static String KEY_ARCHITECTURECODE = "arch";
+    public final static String KEY_PKGCATEGORYCODE = "pkgcat";
+    public final static String KEY_SEARCHEXPRESSION = "srchexpr";
+    public final static String KEY_VIEWCRITERIATYPECODE = "viewcrttyp";
+
+    public final static int PAGESIZE = 15;
+
+    @Resource
+    ServerRuntime serverRuntime;
+
+    @Resource
+    PkgOrchestrationService pkgOrchestrationService;
+
+    /**
+ * <p>This is the entry point for the home page. It will look at the parameters supplied and will
+     * establish what should be displayed.</p>
+     */
+
+    @RequestMapping(method = RequestMethod.GET)
+    public ModelAndView home(
+            HttpServletRequest httpServletRequest,
+ @RequestParam(value=KEY_OFFSET, defaultValue = "0") Integer offset, + @RequestParam(value=KEY_ARCHITECTURECODE, defaultValue = Architecture.CODE_X86) String architectureCode, + @RequestParam(value=KEY_PKGCATEGORYCODE, required=false) String pkgCategoryCode, + @RequestParam(value=KEY_SEARCHEXPRESSION, required=false) String searchExpression, + @RequestParam(value=KEY_VIEWCRITERIATYPECODE, required=false) ViewCriteriaType viewCriteriaType) {
+
+        ObjectContext context = serverRuntime.getContext();
+
+        // ------------------------------
+        // FETCH THE DATA
+
+ PkgSearchSpecification searchSpecification = new PkgSearchSpecification();
+
+        searchSpecification.setOffset(offset);
+        searchSpecification.setLimit(PAGESIZE);
+        searchSpecification.setExpression(searchExpression);
+ searchSpecification.setExpressionType(AbstractSearchSpecification.ExpressionType.CONTAINS);
+
+ Optional<Architecture> architectureOptional = Architecture.getByCode(context, architectureCode);
+
+        if(!architectureOptional.isPresent()) {
+ throw new IllegalStateException("unable to obtain the architecture; " + architectureCode);
+        }
+
+        searchSpecification.setArchitecture(architectureOptional.get());
+
+        Optional<PkgCategory> pkgCategoryOptional = Optional.absent();
+
+        if(null!=pkgCategoryCode) {
+ pkgCategoryOptional = PkgCategory.getByCode(context, pkgCategoryCode);
+        }
+
+ searchSpecification.setNaturalLanguage(MultipageHelper.deriveNaturalLanguage(context, httpServletRequest));
+
+ switch(null==viewCriteriaType ? ViewCriteriaType.FEATURED : viewCriteriaType) {
+
+            case FEATURED:
+ searchSpecification.setSortOrdering(PkgSearchSpecification.SortOrdering.PROMINENCE);
+                break;
+
+            case CATEGORIES:
+ searchSpecification.setSortOrdering(PkgSearchSpecification.SortOrdering.NAME);
+
+                if(!pkgCategoryOptional.isPresent()) {
+ throw new IllegalStateException("the pkg category code was unable to be found; " + pkgCategoryCode);
+                }
+
+ searchSpecification.setPkgCategory(pkgCategoryOptional.get());
+
+                break;
+
+            case ALL:
+ searchSpecification.setSortOrdering(PkgSearchSpecification.SortOrdering.NAME);
+                break;
+
+            case MOSTVIEWED:
+ searchSpecification.setSortOrdering(PkgSearchSpecification.SortOrdering.VERSIONVIEWCOUNTER);
+                break;
+
+            case MOSTRECENT:
+ searchSpecification.setSortOrdering(PkgSearchSpecification.SortOrdering.VERSIONCREATETIMESTAMP);
+                break;
+
+            default:
+ throw new IllegalStateException("unhandled view criteria type");
+
+        }
+
+ Long totalPkgVersions = pkgOrchestrationService.total(context, searchSpecification);
+
+        if(searchSpecification.getOffset() > totalPkgVersions) {
+            searchSpecification.setOffset(totalPkgVersions.intValue());
+        }
+
+ List<PkgVersion> pkgVersions = pkgOrchestrationService.search(context, searchSpecification, null);
+
+        // ------------------------------
+        // GENERATE OUTPUT
+
+        HomeData data = new HomeData();
+
+        final Set<String> excludedArchitectureCode = ImmutableSet.of(
+                Architecture.CODE_ANY,
+                Architecture.CODE_SOURCE
+        );
+
+        data.setAllArchitectures(Lists.newArrayList(
+                ImmutableList.copyOf(
+                        Iterables.filter(
+                                Architecture.getAll(context),
+                                new Predicate<Architecture>() {
+                                    @Override
+ public boolean apply(Architecture input) { + return !excludedArchitectureCode.contains(input.getCode());
+                                    }
+                                }
+                        )
+                )
+        ));
+
+        data.setArchitecture(architectureOptional.get());
+
+        data.setAllPkgCategories(PkgCategory.getAll(context));
+ data.setPkgCategory(pkgCategoryOptional.isPresent() ? pkgCategoryOptional.get() : PkgCategory.getAll(context).get(0));
+
+ data.setAllViewCriteriaTypes(ImmutableList.copyOf(ViewCriteriaType.values()));
+        data.setViewCriteriaType(viewCriteriaType);
+
+        data.setSearchExpression(searchExpression);
+        data.setPkgVersions(pkgVersions);
+ data.setPagination(new Pagination(totalPkgVersions.intValue(), offset, PAGESIZE));
+
+        ModelAndView result = new ModelAndView("multipage/home");
+        result.addObject("data", data);
+
+        return result;
+    }
+
+    /**
+     * <p>This is the data model for the page to be rendered from.</p>
+     */
+
+    public static class HomeData {
+
+        private List<PkgVersion> pkgVersions;
+
+        private List<Architecture> allArchitectures;
+
+        private List<PkgCategory> allPkgCategories;
+
+        private List<ViewCriteriaType> allViewCriteriaTypes;
+
+        private Architecture architecture;
+
+        private PkgCategory pkgCategory;
+
+        private String searchExpression;
+
+        private ViewCriteriaType viewCriteriaType;
+
+        private Pagination pagination;
+
+        public List<PkgVersion> getPkgVersions() {
+            return pkgVersions;
+        }
+
+        public void setPkgVersions(List<PkgVersion> pkgVersions) {
+            this.pkgVersions = pkgVersions;
+        }
+
+        public List<Architecture> getAllArchitectures() {
+            return allArchitectures;
+        }
+
+ public void setAllArchitectures(List<Architecture> allArchitectures) {
+            this.allArchitectures = allArchitectures;
+        }
+
+        public Architecture getArchitecture() {
+            return architecture;
+        }
+
+        public void setArchitecture(Architecture architecture) {
+            this.architecture = architecture;
+        }
+
+        public String getSearchExpression() {
+            return searchExpression;
+        }
+
+        public void setSearchExpression(String searchExpression) {
+            this.searchExpression = searchExpression;
+        }
+
+        public List<PkgCategory> getAllPkgCategories() {
+            return allPkgCategories;
+        }
+
+ public void setAllPkgCategories(List<PkgCategory> allPkgCategories) {
+            this.allPkgCategories = allPkgCategories;
+        }
+
+        public List<ViewCriteriaType> getAllViewCriteriaTypes() {
+            return allViewCriteriaTypes;
+        }
+
+ public void setAllViewCriteriaTypes(List<ViewCriteriaType> allViewCriteriaTypes) {
+            this.allViewCriteriaTypes = allViewCriteriaTypes;
+        }
+
+        public PkgCategory getPkgCategory() {
+            return pkgCategory;
+        }
+
+        public void setPkgCategory(PkgCategory pkgCategory) {
+            this.pkgCategory = pkgCategory;
+        }
+
+        public ViewCriteriaType getViewCriteriaType() {
+            return viewCriteriaType;
+        }
+
+ public void setViewCriteriaType(ViewCriteriaType viewCriteriaType) {
+            this.viewCriteriaType = viewCriteriaType;
+        }
+
+        public Pagination getPagination() {
+            return pagination;
+        }
+
+        public void setPagination(Pagination pagination) {
+            this.pagination = pagination;
+        }
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/controller/ViewPkgController.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.controller;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.haikuos.haikudepotserver.dataobjects.Architecture;
+import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
+import org.haikuos.haikudepotserver.dataobjects.Pkg;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
+import org.haikuos.haikudepotserver.multipage.MultipageConstants;
+import org.haikuos.haikudepotserver.multipage.MultipageHelper;
+import org.haikuos.haikudepotserver.multipage.MultipageObjectNotFoundException;
+import org.haikuos.haikudepotserver.support.VersionCoordinates;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * <p>'Page' for showing a version of a package.</p>
+ */
+
+@Controller
+@RequestMapping(MultipageConstants.PATH_MULTIPAGE + "/pkg")
+public class ViewPkgController {
+
+    @Resource
+    ServerRuntime serverRuntime;
+
+    private String hyphenToNull(String part) {
+        if(null!=part && !part.equals("-")) {
+            return part;
+        }
+
+        return null;
+    }
+
+ @RequestMapping(value = "{name}/{major}/{minor}/{micro}/{preRelease}/{revision}/{architectureCode}", method = RequestMethod.GET)
+    public ModelAndView viewPkg(
+            HttpServletRequest httpServletRequest,
+            @PathVariable(value="name") String pkgName,
+            @PathVariable(value="major") String major,
+            @PathVariable(value="minor") String minor,
+            @PathVariable(value="micro") String micro,
+            @PathVariable(value="preRelease") String preRelease,
+            @PathVariable(value="revision") String revisionStr,
+ @PathVariable(value="architectureCode") String architectureCode) throws MultipageObjectNotFoundException {
+
+        major = hyphenToNull(major);
+        minor = hyphenToNull(minor);
+        micro = hyphenToNull(micro);
+        preRelease = hyphenToNull(preRelease);
+        revisionStr = hyphenToNull(revisionStr);
+
+ Integer revision = null==revisionStr ? null : Integer.parseInt(revisionStr);
+
+        ObjectContext context = serverRuntime.getContext();
+        Optional<Pkg> pkgOptional = Pkg.getByName(context, pkgName);
+
+        if(!pkgOptional.isPresent()) {
+ throw new MultipageObjectNotFoundException(Pkg.class.getSimpleName(), pkgName); // 404
+        }
+
+ Optional<Architecture> architectureOptional = Architecture.getByCode(context, architectureCode);
+
+        if(!architectureOptional.isPresent()) {
+ throw new MultipageObjectNotFoundException(Architecture.class.getSimpleName(), architectureCode);
+        }
+
+        VersionCoordinates coordinates = new VersionCoordinates(
+                Strings.emptyToNull(major),
+                Strings.emptyToNull(minor),
+                Strings.emptyToNull(micro),
+                Strings.emptyToNull(preRelease),
+                revision);
+
+        Optional<PkgVersion> pkgVersionOptional = PkgVersion.getForPkg(
+                context,
+                pkgOptional.get(),
+                architectureOptional.get(),
+                coordinates);
+
+        if(!pkgVersionOptional.isPresent()) {
+ throw new MultipageObjectNotFoundException(PkgVersion.class.getSimpleName(), pkgName + "...");
+        }
+
+        String homeUrl;
+
+        {
+ UriComponentsBuilder builder = UriComponentsBuilder.fromPath(MultipageConstants.PATH_MULTIPAGE); + String naturalLanguageCode = httpServletRequest.getParameter(MultipageConstants.KEY_NATURALLANGUAGECODE);
+
+            if(!Strings.isNullOrEmpty(naturalLanguageCode)) {
+ builder.queryParam(MultipageConstants.KEY_NATURALLANGUAGECODE, naturalLanguageCode);
+            }
+
+            homeUrl = builder.build().toString();
+        }
+
+        ViewPkgVersionData data = new ViewPkgVersionData();
+
+        data.setPkgVersion(pkgVersionOptional.get());
+ data.setCurrentNaturalLanguage(MultipageHelper.deriveNaturalLanguage(context, httpServletRequest));
+        data.setHomeUrl(homeUrl);
+
+        ModelAndView result = new ModelAndView("multipage/viewPkgVersion");
+        result.addObject("data", data);
+
+        return result;
+
+    }
+
+
+    /**
+     * <p>This is the data model for the page to be rendered from.</p>
+     */
+
+    public static class ViewPkgVersionData {
+
+        private PkgVersion pkgVersion;
+
+        private NaturalLanguage currentNaturalLanguage;
+
+        private String homeUrl;
+
+        public PkgVersion getPkgVersion() {
+            return pkgVersion;
+        }
+
+        public void setPkgVersion(PkgVersion pkgVersion) {
+            this.pkgVersion = pkgVersion;
+        }
+
+        public NaturalLanguage getCurrentNaturalLanguage() {
+            return currentNaturalLanguage;
+        }
+
+ public void setCurrentNaturalLanguage(NaturalLanguage currentNaturalLanguage) {
+            this.currentNaturalLanguage = currentNaturalLanguage;
+        }
+
+        public String getHomeUrl() {
+            return homeUrl;
+        }
+
+        public void setHomeUrl(String homeUrl) {
+            this.homeUrl = homeUrl;
+        }
+    }
+
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/NaturalLanguageChooserTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import com.google.common.collect.Lists;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
+import org.haikuos.haikudepotserver.multipage.MultipageConstants;
+import org.haikuos.haikudepotserver.multipage.MultipageHelper;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+import org.springframework.web.servlet.tags.form.TagWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * <p>This tag renders a list of languages allowing the user to choose one of those languages.</p>
+ */
+
+public class NaturalLanguageChooserTag extends RequestContextAwareTag {
+
+    private ObjectContext getObjectContext() {
+ return getRequestContext().getWebApplicationContext().getBean(ServerRuntime.class).getContext();
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        ObjectContext context = getObjectContext();
+        TagWriter tagWriter = new TagWriter(pageContext.getOut());
+ List<NaturalLanguage> naturalLanguages = Lists.newArrayList(NaturalLanguage.getAll(context)); + NaturalLanguage currentNaturalLanguage = MultipageHelper.deriveNaturalLanguage(
+                context,
+                (HttpServletRequest) pageContext.getRequest());
+
+ Collections.sort(naturalLanguages, new Comparator<NaturalLanguage>() {
+                    @Override
+ public int compare(NaturalLanguage o1, NaturalLanguage o2) {
+                        return o1.getCode().compareTo(o2.getCode());
+                    }
+                }
+        );
+
+        tagWriter.startTag("span");
+ tagWriter.writeAttribute("class","multipage-natural-language-chooser");
+
+        for(int i=0;i<naturalLanguages.size();i++) {
+
+            NaturalLanguage naturalLanguage = naturalLanguages.get(i);
+
+            if(0 != i) {
+                tagWriter.appendValue(" ");
+            }
+
+            if(currentNaturalLanguage != naturalLanguage) {
+
+ ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentRequest(); + builder.replaceQueryParam(MultipageConstants.KEY_NATURALLANGUAGECODE, naturalLanguage.getCode());
+
+                tagWriter.startTag("a");
+ tagWriter.writeAttribute("href",builder.build().toString());
+                tagWriter.appendValue(naturalLanguage.getCode());
+                tagWriter.endTag(); // a
+
+            }
+            else {
+                tagWriter.startTag("strong");
+                tagWriter.appendValue(naturalLanguage.getCode());
+                tagWriter.endTag(); // strong
+            }
+
+        }
+
+        tagWriter.endTag(); // span
+
+        return SKIP_BODY;
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PaginationLinksTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import org.haikuos.haikudepotserver.multipage.model.Pagination;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+import org.springframework.web.servlet.tags.form.TagWriter;
+
+import javax.servlet.jsp.JspException;
+import java.util.List;
+
+/**
+ * <p>This is a JSP tag that is able to render some hyperlinks that allow the user to browse around some linear list + * of data items. See {@link org.haikuos.haikudepotserver.multipage.model.Pagination} for the model that produces
+ * the series of page numbers.</p>
+ */
+
+public class PaginationLinksTag extends RequestContextAwareTag {
+
+    private final static int LINK_COUNT_DEFAULT = 10;
+
+    private Integer linkCount;
+
+    private Pagination pagination;
+
+    public Pagination getPagination() {
+        return pagination;
+    }
+
+    public void setPagination(Pagination value) {
+        this.pagination = value;
+    }
+
+    public Integer getLinkCount() {
+        return linkCount;
+    }
+
+    @SuppressWarnings("UnusedDeclaration")
+    public void setLinkCount(Integer linkCount) {
+        this.linkCount = linkCount;
+    }
+
+    private String deriveHref(int targetPage) {
+ ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentRequest(); + builder.replaceQueryParam("o", Integer.toString(targetPage * getPagination().getMax()));
+        return builder.build().toString();
+    }
+
+    private void writeArrow(
+            TagWriter tagWriter,
+            String imageFilename,
+            String cssClassName,
+            String alt,
+            String href) throws JspException {
+        tagWriter.startTag("li");
+        tagWriter.startTag("a");
+        tagWriter.writeAttribute("class", cssClassName);
+        tagWriter.writeAttribute("href",href);
+        tagWriter.writeAttribute("alt",alt);
+        tagWriter.startTag("img");
+        tagWriter.writeAttribute("src", "/img/" + imageFilename);
+        tagWriter.endTag(); // img
+        tagWriter.endTag(); // a
+        tagWriter.endTag(); // li
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        TagWriter tagWriter = new TagWriter(pageContext.getOut());
+
+        Pagination p = getPagination();
+
+        List<String> ulClasses = Lists.newArrayList();
+        ulClasses.add("pagination-control-container");
+
+        if(0==p.getPage()) {
+            ulClasses.add("pagination-control-on-first");
+        }
+
+        if(p.getPage() == p.getPages()-1) {
+            ulClasses.add("pagination-control-on-last");
+        }
+
+        tagWriter.startTag("ul");
+        tagWriter.writeAttribute("class", Joiner.on(' ').join(ulClasses));
+
+        writeArrow(
+                tagWriter,
+                "paginationleft.png",
+                "pagination-control-left",
+                "<--",
+                0==p.getPage() ? "" : deriveHref(p.getPage()-1));
+
+ int[] pageNumbers = p.generateSuggestedPages(null==getLinkCount() ? LINK_COUNT_DEFAULT : getLinkCount());
+
+        for(int pageNumber : pageNumbers) {
+            tagWriter.startTag("li");
+
+            if (pageNumber == p.getPage()) {
+                tagWriter.startTag("span");
+ tagWriter.writeAttribute("class", "pagination-control-currentpage");
+                tagWriter.writeAttribute("href", "");
+            } else {
+                tagWriter.startTag("a");
+                tagWriter.writeAttribute("href", deriveHref(pageNumber));
+            }
+
+            tagWriter.appendValue(Integer.toString(pageNumber + 1));
+            tagWriter.endTag(); // a
+            tagWriter.endTag(); // li
+        }
+
+        writeArrow(
+                tagWriter,
+                "paginationright.png",
+                "pagination-control-right",
+                "-->",
+ p.getPage() == p.getPages()-1 ? "" : deriveHref(p.getPage()+1));
+
+        tagWriter.endTag(); // ul
+
+        return SKIP_BODY;
+    }
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PkgIconTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.net.MediaType;
+import org.apache.cayenne.ObjectContext;
+import org.haikuos.haikudepotserver.dataobjects.PkgIcon;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+import org.springframework.web.servlet.tags.form.TagWriter;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * <p>Renders HTML for a package version's icon.</p>
+ */
+
+public class PkgIconTag extends RequestContextAwareTag {
+
+    private PkgVersion pkgVersion;
+
+    private int size = 16;
+
+    public PkgVersion getPkgVersion() {
+        return pkgVersion;
+    }
+
+    public void setPkgVersion(PkgVersion pkgVersion) {
+        this.pkgVersion = pkgVersion;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public void setSize(int size) {
+        this.size = size;
+    }
+
+    private String getUrl() {
+        ObjectContext context = getPkgVersion().getObjectContext();
+ Optional<org.haikuos.haikudepotserver.dataobjects.MediaType> pngOptional = + org.haikuos.haikudepotserver.dataobjects.MediaType.getByCode(context, MediaType.PNG.toString()); + Optional<PkgIcon> pkgIconOptional = pkgVersion.getPkg().getPkgIcon(pngOptional.get(), getSize());
+
+        if(pkgIconOptional.isPresent()) {
+            return
+                    UriComponentsBuilder.newInstance()
+ .pathSegment("pkgicon", getPkgVersion().getPkg().getName() + ".png")
+                    .queryParam("f","true")
+                            .queryParam("s",Integer.toString(getSize()))
+                    
.queryParam("m",Long.toString(getPkgVersion().getPkg().getModifyTimestamp().getTime()))
+                    .build()
+                    .toString();
+        }
+        else {
+            switch(size) {
+                case 16:
+                    return "/img/generic16.png";
+
+                case 32:
+                    return "/img/generic32.png";
+
+                default:
+ throw new IllegalStateException("unknown size for default icon");
+            }
+        }
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        Preconditions.checkNotNull(pkgVersion);
+        Preconditions.checkState(getSize()==16 || getSize()==32);
+
+        TagWriter tagWriter = new TagWriter(pageContext.getOut());
+
+        tagWriter.startTag("img");
+        tagWriter.writeAttribute("src", getUrl());
+        tagWriter.writeAttribute("alt", "icon");
+        tagWriter.endTag();
+
+        return SKIP_BODY;
+    }
+
+}
+
+
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PkgVersionLabelTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import com.google.common.base.Preconditions;
+import com.google.common.html.HtmlEscapers;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+import org.springframework.web.servlet.tags.form.TagWriter;
+
+/**
+ * <P>Renders the version of coordinates of a package version.</P>
+ */
+
+public class PkgVersionLabelTag extends RequestContextAwareTag {
+
+    private PkgVersion pkgVersion;
+
+    public PkgVersion getPkgVersion() {
+        return pkgVersion;
+    }
+
+    public void setPkgVersion(PkgVersion pkgVersion) {
+        this.pkgVersion = pkgVersion;
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        Preconditions.checkNotNull(pkgVersion);
+
+        TagWriter tagWriter = new TagWriter(pageContext.getOut());
+
+        tagWriter.startTag("span");
+ tagWriter.appendValue(HtmlEscapers.htmlEscaper().escape(pkgVersion.toVersionCoordinates().toString()));
+        tagWriter.endTag();
+
+        return SKIP_BODY;
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PkgVersionLinkTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.html.HtmlEscapers;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
+import org.haikuos.haikudepotserver.multipage.MultipageConstants;
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.JspWriter;
+import java.io.IOException;
+
+/**
+ * <p>This tag renders a link to a package version.</p>
+ */
+
+public class PkgVersionLinkTag extends RequestContextAwareTag {
+
+    private PkgVersion pkgVersion;
+
+    public PkgVersion getPkgVersion() {
+        return pkgVersion;
+    }
+
+    public void setPkgVersion(PkgVersion pkgVersion) {
+        this.pkgVersion = pkgVersion;
+    }
+
+    private String emptyToHyphen(String part) {
+        if(Strings.isNullOrEmpty(part)) {
+            return "-";
+        }
+
+        return part;
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        Preconditions.checkNotNull(getPkgVersion());
+
+        JspWriter jspWriter = pageContext.getOut();
+
+ UriComponentsBuilder builder = UriComponentsBuilder.fromPath(String.format(
+                "%s/pkg/%s/%s/%s/%s/%s/%s/%s",
+                MultipageConstants.PATH_MULTIPAGE,
+                pkgVersion.getPkg().getName(),
+                emptyToHyphen(pkgVersion.getMajor()),
+                emptyToHyphen(pkgVersion.getMinor()),
+                emptyToHyphen(pkgVersion.getMicro()),
+                emptyToHyphen(pkgVersion.getPreRelease()),
+ null == pkgVersion.getRevision() ? "-" : pkgVersion.getRevision().toString(),
+                pkgVersion.getArchitecture().getCode()));
+
+ String naturalLanguageCode = pageContext.getRequest().getParameter(MultipageConstants.KEY_NATURALLANGUAGECODE);
+
+        if(!Strings.isNullOrEmpty(naturalLanguageCode)) {
+            builder.queryParam(
+                    MultipageConstants.KEY_NATURALLANGUAGECODE,
+                    naturalLanguageCode);
+        }
+
+        jspWriter.print("<a href=\"");
+ jspWriter.print(HtmlEscapers.htmlEscaper().escape(builder.build().toString()));
+        jspWriter.print("\">");
+
+        return EVAL_BODY_INCLUDE;
+    }
+
+    @Override
+    public int doEndTag() throws JspException {
+        JspWriter jspWriter = pageContext.getOut();
+
+        try {
+            jspWriter.print("</a>");
+        }
+        catch(IOException ioe) {
+ throw new JspException("unable to write the end of the pkg version link", ioe);
+        }
+
+        return EVAL_PAGE;
+    }
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PlainTextContentTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.html.HtmlEscapers;
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+
+import java.util.regex.Pattern;
+
+/**
+ * <p>This tag renders some text with newlines turned into valid HTML structure.</p>
+ */
+
+public class PlainTextContentTag extends RequestContextAwareTag {
+
+ private final static Pattern PATTERN_NEWLINE = Pattern.compile("\\n\\d| \\d\\n|\\n");
+
+    private String value;
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        if (!Strings.isNullOrEmpty(value)) {
+
+            pageContext.getOut().print(Joiner.on("<br/>\n").join(
+                    Iterables.transform(
+                            Splitter.on(PATTERN_NEWLINE).split(value),
+                            new Function<String, String>() {
+                                @Override
+                                public String apply(String input) {
+ return HtmlEscapers.htmlEscaper().escape(input);
+                                }
+                            }
+                    )
+            ));
+        }
+
+        return SKIP_BODY;
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/RatingIndicatorTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+import org.springframework.web.servlet.tags.form.TagWriter;
+
+/**
+ * <p>This tag renders an HTML structure that represents a number of stars that represents a user rating value.
+ * This value has to be within 0 to 5.</p>
+ */
+
+public class RatingIndicatorTag extends RequestContextAwareTag {
+
+    private Float value;
+
+    public Float getValue() {
+        return value;
+    }
+
+    public void setValue(Float value) {
+        this.value = value;
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        TagWriter tagWriter = new TagWriter(pageContext.getOut());
+
+        if(null!=getValue()) {
+
+            if(getValue() < 0f || getValue() > 5f) {
+ throw new IllegalStateException("the value for the rating indicator must be [0..5]");
+            }
+
+            tagWriter.startTag("span");
+            tagWriter.writeAttribute("class","rating-indicator");
+
+            int value = (int) (getValue() * 2f);
+
+            for(int i=0;i<5;i++) {
+                tagWriter.startTag("img");
+
+                switch(value) {
+
+                    case 1:
+ tagWriter.writeAttribute("src","/img/starhalf.png");
+                        tagWriter.writeAttribute("alt","o");
+                        break;
+
+                    case 0:
+                        tagWriter.writeAttribute("src","/img/staroff.png");
+                        tagWriter.writeAttribute("alt",".");
+                        break;
+
+                    default:
+                        tagWriter.writeAttribute("src","/img/staron.png");
+                        tagWriter.writeAttribute("alt","*");
+                        break;
+
+                }
+
+                tagWriter.endTag();
+
+                value = Math.max(0,value-2);
+            }
+
+            tagWriter.endTag();
+        }
+
+        return SKIP_BODY;
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/TimestampTag.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.markup;
+
+import com.google.common.html.HtmlEscapers;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.DateTimeFormatterBuilder;
+import org.springframework.web.servlet.tags.RequestContextAwareTag;
+import org.springframework.web.servlet.tags.form.TagWriter;
+
+import java.util.Date;
+
+/**
+ * <p>This tag will render a {@link java.util.Date} as a timestamp.</p>
+ */
+
+public class TimestampTag extends RequestContextAwareTag {
+
+    // might be an idea to put this somewhere else if we need it again?
+    private final static DateTimeFormatter FORMATTER =
+            new DateTimeFormatterBuilder()
+            .appendYear(4,4)
+            .appendLiteral('-')
+            .appendMonthOfYear(2)
+            .appendLiteral('-')
+            .appendDayOfMonth(2)
+            .appendLiteral(' ')
+            .appendHourOfDay(2)
+            .appendLiteral(':')
+            .appendMinuteOfHour(2)
+            .appendLiteral(':')
+            .appendSecondOfMinute(2)
+            .toFormatter()
+            .withZoneUTC();
+
+    private java.util.Date value;
+
+    public Date getValue() {
+        return value;
+    }
+
+    public void setValue(Date value) {
+        this.value = value;
+    }
+
+    @Override
+    protected int doStartTagInternal() throws Exception {
+
+        TagWriter tagWriter = new TagWriter(pageContext.getOut());
+
+        if(null!=getValue()) {
+            tagWriter.startTag("span");
+ tagWriter.appendValue(HtmlEscapers.htmlEscaper().escape(FORMATTER.print(getValue().getTime())));
+            tagWriter.endTag();
+        }
+
+        return SKIP_BODY;
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/model/Pagination.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.model;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * <P>This object aims to provide the pagination within a list of items. It aims to be more or less
+ * like the "paginationcontroldirective.js" behaviour.</P>
+ */
+
+public class Pagination {
+
+    private int offset;
+    private int total;
+    private int max;
+
+    /**
+     * @param total is the total number of items in the entire result set
+     * @param offset if the offset from 0 into the total number of items
+     * @param max is the maximum number of items to show on a page
+     */
+
+    public Pagination(int total, int offset, int max) {
+        Preconditions.checkState(offset >= 0);
+        Preconditions.checkState(total >= 0);
+        Preconditions.checkState(offset < total);
+        Preconditions.checkState(max >= 1);
+        this.offset = offset;
+        this.total = total;
+        this.max = max;
+    }
+
+    // ------------------
+    // ACCESSORS
+
+    public int getOffset() {
+        return offset;
+    }
+
+    public void setOffset(int offset) {
+        this.offset = offset;
+    }
+
+    public int getTotal() {
+        return total;
+    }
+
+    public void setTotal(int total) {
+        this.total = total;
+    }
+
+    public int getMax() {
+        return max;
+    }
+
+    public void setMax(int max) {
+        this.max = max;
+    }
+
+    // ------------------
+    // PAGE CONTROL
+
+    /**
+ * <p>This returns the current page in the pagination based on the offset.</p>
+     */
+
+    public int getPage() {
+        return (offset / max);
+    }
+
+    /**
+     * <p>This returns the total number of pages in the pagination.</p>
+     */
+
+    public int getPages() {
+        return (total / max) + (0 != total % max ? 1 : 0);
+    }
+
+    private int[] linearSeries(int count) {
+        int[] result = new int[count];
+        for(int i=0;i<count;i++) { result[i] = i; }
+        return result;
+    }
+
+    private void fanFillRight(int[] result, int startI) {
+
+        int pages = getPages() - 1;
+ int page = getPage() + 1; // assume the actual page has been set already
+        int len = result.length - startI;
+
+        for (int i = 0; i < len; i++) {
+            float p = (float) i / (float) (len - 1);
+            float f = p * p;
+            result[startI + i] = Math.max(
+                    result[(startI + i) - 1] + 1,
+                    page + (int) (f * (float) (pages - page)));
+        }
+
+    }
+
+    private void fanFillLeft(int[] result, int startI) {
+
+ int page = getPage() - 1; // assume the actual page has been set already
+
+        for (int i = 0; i < startI; i++) {
+            float p = (float) i / (float) startI;
+            float f = p * p;
+            result[startI - i] = Math.min(
+                    result[(startI - i) + 1] - 1,
+                    page - (int) (f * (float) page));
+        }
+
+    }
+
+    /**
+ * <p>This method will return an integer array containing item offsets that can be taken to be handy + * pages that the user might like to jump to. This can then be used to present a list of pages within + * a list of data. The returned pages try to present a set of smart pages to jump to and may not be + * strictly linear. If there is only one page then you may be returned an empty array.</p>
+     */
+
+    public int[] generateSuggestedPages(int count) {
+
+ Preconditions.checkState(count > 3 && 0==count%2,"the count of pages must be more than 3 and an even number");
+
+        int pages = getPages();
+
+        if(1==pages) {
+            return new int[] { 0 };
+        }
+
+        int page = getPage(); // current page.
+
+        if(pages <= count) {
+            return linearSeries(pages);
+        }
+
+        int[] result = new int[count];
+        int middleI = count / 2;
+
+        if(page < middleI) {
+
+            for(int i=0;i<=page;i++) {
+                result[i] = i;
+            }
+
+            fanFillRight(result, page+1);
+
+        }
+        else {
+
+            int remainder = pages - page;
+
+            if(remainder < middleI) {
+
+                for(int i=0;i<remainder;i++) {
+                    result[result.length - (i + 1)] = (pages - 1) - i;
+                }
+
+                fanFillLeft(result,result.length-(remainder + 1));
+
+            }
+            else {
+                result[middleI] = page;
+                fanFillRight(result, middleI+1);
+                fanFillLeft(result, middleI-1);
+            }
+
+        }
+
+        return result;
+    }
+
+    // ------------------
+    // STANDARD STUFF
+
+    @Override
+    public String toString() {
+        return "pagination; " + offset + " in " + total;
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/package-info.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,6 @@
+/**
+ * <p>This package is concerned with the presentation of a simplified user interface that is driven by vanilla + * web pages as opposed to the "single page" approach taken in the main user interface for the application.</p>
+ */
+
+package org.haikuos.haikudepotserver.multipage;
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/singlepage/controller/EntryPointController.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.singlepage.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * <p>This controller renders the default HTML entry point into the application. As this is <em>generally</em> a + * single page application, this controller will just render that single page.</p>
+ */
+
+@Controller
+@RequestMapping("/")
+public class EntryPointController {
+
+    @RequestMapping(method = RequestMethod.GET)
+    public String entryPoint() {
+        return "singlepage/launch";
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/singlepage/package-info.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,6 @@
+/**
+ * <p>This package is concerned with elements that are strictly related to the launch of the single-page
+ * application that forms the main user interface for the application.</p>
+ */
+
+package org.haikuos.haikudepotserver.singlepage;
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/RobotController.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.support.web;
+
+import com.google.common.net.MediaType;
+import org.haikuos.haikudepotserver.multipage.MultipageConstants;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+// https://developers.google.com/webmasters/control-crawl-index/docs/robots_txt
+
+@Controller
+public class RobotController {
+
+    @RequestMapping(value = "/robots.txt", method = RequestMethod.GET)
+ public void robotResponse(HttpServletResponse response) throws IOException {
+        response.setContentType(MediaType.PLAIN_TEXT_UTF_8.toString());
+
+        PrintWriter writer = response.getWriter();
+
+        writer.print("user-agent: *\n");
+        writer.print("allow: ");
+        writer.print(MultipageConstants.PATH_MULTIPAGE);
+        writer.print("\n");
+
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/includes/favicons.jsp Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,3 @@
+<link rel="icon" type="image/png" href="/img/haikudepot16.png" sizes="16x16"> +<link rel="icon" type="image/png" href="/img/haikudepot32.png" sizes="32x32"> +<link rel="icon" type="image/png" href="/img/haikudepot64.png" sizes="64x64">
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/multipage.tld Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<taglib>
+    <tlib-version>1.0</tlib-version>
+    <jsp-version>1.2</jsp-version>
+    <short-name>multipage</short-name>
+
+    <tag>
+        <name>paginationLinks</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.PaginationLinksTag</tag-class>
+        <description>
+ Displays a series of links that allow the user to choose one page from a number of pages
+            of data.
+        </description>
+        <attribute>
+            <name>pagination</name>
+            <required>true</required>
+ <rtexprvalue>true</rtexprvalue> <!-- can use SPEL expressions -->
+        </attribute>
+    </tag>
+
+    <tag>
+        <name>pkgIcon</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.PkgIconTag</tag-class>
+        <description>
+            Renders an image tag with the image of a package.
+        </description>
+        <attribute>
+            <name>pkgVersion</name>
+            <required>true</required>
+ <rtexprvalue>true</rtexprvalue> <!-- can use SPEL expressions -->
+        </attribute>
+        <attribute>
+            <name>size</name>
+            <required>true</required>
+        </attribute>
+    </tag>
+
+    <tag>
+        <name>pkgVersionLabel</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.PkgVersionLabelTag</tag-class>
+        <description>
+            Renders a label indicating the version of a package
+        </description>
+        <attribute>
+            <name>pkgVersion</name>
+            <required>true</required>
+ <rtexprvalue>true</rtexprvalue> <!-- can use SPEL expressions -->
+        </attribute>
+    </tag>
+
+    <tag>
+        <name>pkgVersionLink</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.PkgVersionLinkTag</tag-class>
+        <description>
+ A hyperlink that references the page that renders the pkg version
+        </description>
+        <attribute>
+            <name>pkgVersion</name>
+            <required>true</required>
+ <rtexprvalue>true</rtexprvalue> <!-- can use SPEL expressions -->
+        </attribute>
+    </tag>
+
+    <tag>
+        <name>timestamp</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.TimestampTag</tag-class>
+        <description>
+            Renders a string indicating a timestamp
+        </description>
+        <attribute>
+            <name>value</name>
+            <required>true</required>
+ <rtexprvalue>true</rtexprvalue> <!-- can use SPEL expressions -->
+        </attribute>
+    </tag>
+
+    <tag>
+        <name>ratingIndicator</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.RatingIndicatorTag</tag-class>
+        <description>
+ Renders a series of images indicating the rating aggregate [0..5]
+        </description>
+        <attribute>
+            <name>value</name>
+            <required>true</required>
+ <rtexprvalue>true</rtexprvalue> <!-- can use SPEL expressions -->
+        </attribute>
+    </tag>
+
+    <tag>
+        <name>naturalLanguageChooser</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.NaturalLanguageChooserTag</tag-class>
+        <description>
+            Allows the user to select their natural language.
+        </description>
+    </tag>
+
+    <tag>
+        <name>plainTextContent</name>
+ <tag-class>org.haikuos.haikudepotserver.multipage.markup.PlainTextContentTag</tag-class>
+        <description>
+            Renders text that may contain newlines niely in html form.
+        </description>
+        <attribute>
+            <name>value</name>
+            <required>true</required>
+ <rtexprvalue>true</rtexprvalue> <!-- can use SPEL expressions -->
+        </attribute>
+    </tag>
+
+</taglib>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/home.jsp Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,170 @@
+<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
+<%@include file="/WEB-INF/views/multipage/includes/prelude.jsp"%>
+
+<html>
+
+<head>
+
+    <title>Haiku Depot Web</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <%@include file="/WEB-INF/includes/favicons.jsp"%>
+
+    <%-- will use the same CSS as the main application --%>
+    <jwr:style src="/bundles/app.css"></jwr:style>
+
+</head>
+
+<body>
+<%@include file="includes/banner.jsp"%>
+
+<div class="container">
+    <div class="content-container home">
+
+        <form method="get" action="/multipage">
+            <div id="search-criteria-container">
+                <div>
+                    <select name="arch">
+ <c:forEach items="${data.allArchitectures}" var="anArchitecture">
+                            <option
+ <c:if test="${anArchitecture == data.architecture}">
+                                        selected="selected"
+                                    </c:if>
+                                    value="${anArchitecture.code}">
+ <c:out value="${anArchitecture.code}"></c:out>
+                            </option>
+                        </c:forEach>
+                    </select>
+                </div>
+                <div>
+                    <select name="viewcrttyp">
+ <c:forEach items="${data.allViewCriteriaTypes}" var="aViewCriteriaType">
+                            <option
+ <c:if test="${aViewCriteriaType == data.viewCriteriaType}">
+                                        selected="selected"
+                                    </c:if>
+                                    value="${aViewCriteriaType.name()}">
+ <spring:message code="${aViewCriteriaType.getTitleKey()}"></spring:message>
+                            </option>
+                        </c:forEach>
+                    </select>
+                </div>
+                <div>
+                    <select name="pkgcat">
+ <c:forEach items="${data.allPkgCategories}" var="aPkgCategory">
+                            <option
+ <c:if test="${aPkgCategory == data.pkgCategory}">
+                                        selected="selected"
+                                    </c:if>
+                                    value="${aPkgCategory.code}">
+ <spring:message code="${aPkgCategory.getTitleKey()}"></spring:message>
+                            </option>
+                        </c:forEach>
+                    </select>
+                </div>
+                <div>
+                    <input
+                            type="text"
+                            placeholder="zlib"
+                            name="srchexpr"
+                            value="${data.searchExpression}">
+
+                    <button type="submit">
+ <spring:message code="home.searchButton.title"></spring:message>
+                    </button>
+                </div>
+            </div>
+        </form>
+
+        <!-- RESULTS -->
+
+        <div id="search-results-container">
+
+            <c:if test="${empty data.pkgVersions}">
+                <div class="info-container">
+ <strong><message key="home.noResults.title"></message>;</strong>
+                    <message key="home.noResults.description"></message>
+                </div>
+            </c:if>
+
+            <c:if test="${not empty data.pkgVersions}">
+                <div class="table-general-container">
+
+                    <div class="table-general-pagination-container">
+ <multipage:paginationLinks pagination="${data.pagination}"></multipage:paginationLinks>
+                    </div>
+
+                    <div class="muted">
+                        <c:out value="${data.pagination.total}"></c:out>
+                        <c:choose>
+                            <c:when test="${1==data.pagination.total}">
+ <spring:message code="gen.pkg.title"></spring:message>
+                            </c:when>
+                            <c:otherwise>
+ <spring:message code="gen.pkg.title.plural"></spring:message>
+                            </c:otherwise>
+                        </c:choose>
+                    </div>
+
+                    <table class="table-general">
+                        <thead>
+                        <th></th>
+ <th><spring:message code="gen.pkg.title"></spring:message></th> + <th><spring:message code="home.table.rating.title"></spring:message></th> + <th><spring:message code="home.table.version.title"></spring:message></th> + <c:if test="${'MOSTRECENT'==data.viewCriteriaType.name()}"> + <th><spring:message code="home.table.approximateVersionDate.title"></spring:message></th>
+                        </c:if>
+ <c:if test="${'MOSTVIEWED'==data.viewCriteriaType.name()}"> + <th><spring:message code="home.table.versionViewCounter.title"></spring:message></th>
+                        </c:if>
+                        </thead>
+                        <tbody>
+ <c:forEach items="${data.pkgVersions}" var="pkgVersion">
+                            <tr>
+                                <td>
+ <multipage:pkgIcon size="16" pkgVersion="${pkgVersion}"></multipage:pkgIcon>
+                                </td>
+                                <td>
+ <multipage:pkgVersionLink pkgVersion="${pkgVersion}"> + <c:out value="${pkgVersion.pkg.name}"></c:out>
+                                    </multipage:pkgVersionLink>
+                                </td>
+                                <td>
+ <c:if test="${not empty pkgVersion.pkg.derivedRating}"> + <multipage:ratingIndicator value="${pkgVersion.pkg.derivedRating}"></multipage:ratingIndicator>
+                                    </c:if>
+                                </td>
+                                <td>
+ <multipage:pkgVersionLabel pkgVersion="${pkgVersion}"></multipage:pkgVersionLabel>
+                                </td>
+ <c:if test="${'MOSTRECENT'==data.viewCriteriaType.name()}">
+                                    <td>
+                                        <span class="muted">
+ <multipage:timestamp value="${pkgVersion.createTimestamp}"></multipage:timestamp>
+                                        </span>
+                                    </td>
+                                </c:if>
+ <c:if test="${'MOSTVIEWED'==data.viewCriteriaType.name()}">
+                                    <td>
+                                        <span class="muted">
+ <c:out value="${pkgVersion.viewCounter}"></c:out>
+                                        </span>
+                                    </td>
+                                </c:if>
+                            </tr>
+                        </c:forEach>
+                        </tbody>
+                    </table>
+
+                </div>
+            </c:if>
+        </div>
+
+    </div>
+</div>
+
+<div class="footer"></div>
+
+</body>
+
+</html>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/includes/banner.jsp Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,24 @@
+<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
+
+<div>
+    <div id="banner-container">
+
+    <span id="banner-title" class="multipage-banner-title">
+ <div>Haiku Depot Server <span><spring:message code="multipage.banner.title.suffix"></spring:message></span></div>
+    </span>
+
+        <div id="banner-actions" class="multipage-banner-actions">
+            <div id="banner-multipage-note">
+ <spring:message code="multipage.banner.note"></spring:message>;
+                <a href="/">
+ <spring:message code="multipage.banner.note.full"></spring:message>
+                </a>
+            </div>
+            <div>
+ <multipage:naturalLanguageChooser></multipage:naturalLanguageChooser>
+            </div>
+        </div>
+
+    </div>
+
+</div>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/includes/prelude.jsp Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,5 @@
+<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"; %>
+<%@ taglib prefix="jwr" uri="http://jawr.net/tags"; %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
+<%@ taglib prefix="multipage" uri="/WEB-INF/multipage.tld"%>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,83 @@
+<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
+<%@include file="/WEB-INF/views/multipage/includes/prelude.jsp"%>
+
+<html>
+
+<head>
+
+    <title>Haiku Depot Web</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <%@include file="/WEB-INF/includes/favicons.jsp"%>
+
+    <%-- will use the same CSS as the main application --%>
+    <jwr:style src="/bundles/app.css"></jwr:style>
+
+</head>
+
+<body>
+<%@include file="includes/banner.jsp"%>
+
+<div class="container">
+
+    <div id="breadcrumbs-container">
+        <ul>
+            <li>
+                <a href="<c:out value="${data.homeUrl}"></c:out>">
+ <spring:message code="breadcrumb.home.title"></spring:message>
+                </a>
+            </li>
+            <li>
+ <span ng-show="isItemActive(item)"><c:out value="${data.pkgVersion.pkg.name}"></c:out></span>
+            </li>
+        </ul>
+    </div>
+
+    <div class="content-container">
+
+        <div id="pkg-title">
+            <div id="pkg-title-icon">
+ <multipage:pkgIcon pkgVersion="${data.pkgVersion}" size="32"/>
+            </div>
+            <div id="pkg-title-text">
+ <h1><c:out value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage).summary}"/></h1>
+                <div class="muted">
+                    <small>
+                        <c:out value="${data.pkgVersion.pkg.name}"></c:out>
+                        -
+ <multipage:pkgVersionLabel pkgVersion="${data.pkgVersion}"></multipage:pkgVersionLabel>
+                        -
+ <c:out value="${data.pkgVersion.architecture.code}"></c:out>
+                    </small>
+                </div>
+            </div>
+        </div>
+
+ <c:if test="${data.pkgVersion.isLatest && not empty data.pkgVersion.pkg.derivedRating}">
+            <div class="pkg-rating-indicator-container">
+ <multipage:ratingIndicator value="${data.pkgVersion.pkg.derivedRating}"></multipage:ratingIndicator>
+              <span class="pkg-ratings-indicator-sample">
+                <small>
+                    <spring:message
+                            code="viewPkg.derivedUserRating.sampleSize"
+ arguments="${data.pkgVersion.pkg.derivedRatingSampleSize}">
+                    </spring:message>
+                </small>
+              </span>
+            </div>
+        </c:if>
+
+        <div id="pkg-description-container">
+            <p>
+ <multipage:plainTextContent value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage).description}"></multipage:plainTextContent>
+            </p>
+        </div>
+
+    </div>
+
+</div>
+
+<div class="footer"></div>
+
+</body>
+
+</html>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/singlepage/launch.jsp Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,44 @@
+<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"; %>
+<%@ taglib prefix="jwr" uri="http://jawr.net/tags"; %>
+
+<%--
+This is a single page application and this is essentially the 'single page'. It boots-up some libraries and other +web-resources and then this starts the java-script single page environment driven by the AngularJS library.
+--%>
+
+<html ng-app="haikudepotserver">
+
+<head>
+
+    <title>Haiku Depot Web</title>
+
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <%@include file="/WEB-INF/includes/favicons.jsp"%>
+
+    <jwr:script src="/bundles/libs.js"></jwr:script>
+    <jwr:script src="/bundles/app.js"></jwr:script>
+    <jwr:style src="/bundles/app.css"></jwr:style>
+
+</head>
+
+<body>
+
+<%@include file="/WEB-INF/includes/unsupported.jsp"%>
+
+<banner></banner>
+
+<div class="container">
+    <div ng-view></div>
+</div>
+
+<%--
+This IFRAME can be used by application logic to cause a download to occur.
+--%>
+
+<iframe id="download-iframe" style="display:none"></iframe>
+
+</body>
+
+</html>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/multipage/banner.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,35 @@
+/*
+Styles in this file are targeting the simplified multi-page interface.
+*/
+
+/*
+Overrides a setting on #banner-title
+*/
+
+.multipage-banner-title {
+ background: url('/img/haikudepot32.png') no-repeat 32px 4px transparent;
+}
+
+#banner-title > div > span {
+    font-family: serif;
+    font-weight: lighter;
+    font-style: italic;
+    color: rgba(255, 255, 255, 0.50)
+}
+
+.multipage-banner-actions {
+    text-align: right;
+}
+
+#banner-multipage-note > a {
+    color: rgba(255,255,255,0.75);
+}
+
+#banner-multipage-note {
+    font-size: 10pt;
+    color: rgba(255,255,255,0.75);
+}
+
+#banner-container .multipage-natural-language-chooser > a {
+    color: rgba(255,255,255,0.75);
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/addedituserrating.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,3 @@
+form textarea {
+    width: 100%
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/banner.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,67 @@
+/*
+These styles are related to the banner at the top of the screen containing the title of the application
+as well as the status of the authenticated user.
+*/
+
+#banner-container {
+    position: relative;
+    margin: 0;
+    background-color: #336698;
+    width: 100%;
+    height: 40px;
+    padding: 0px;
+    border-bottom: 1px solid black;
+}
+
+#banner-title {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    padding-left: 68px;
+    height: 40px;
+    background: url('/img/haikudepot.svg') no-repeat 32px 4px transparent;
+    background-size: 32px 32px;
+}
+
+#banner-title div {
+    position: relative;
+    top: 8px;
+    left: 0px;
+    color: white;
+    font-weight: bold;
+    font-size: 24px;
+    vertical-align: middle;
+}
+
+#banner-actions {
+    position: absolute;
+    top: 6px;
+    right: 0px;
+    overflow: hidden;
+    padding-right: 32px;
+    color: white;
+    overflow: hidden;
+}
+
+#banner-actions > .banner-actions-state-container {
+    margin-right: 6px;
+    display: inline-block;
+}
+
+#banner-actions > .banner-actions-state-container > span {
+   line-height: 18px;
+   vertial-align: middle;
+}
+
+#banner-container a {
+    color: white;
+}
+
+#banner-actions button {
+    background-color: transparent;
+    border-color: white;
+    border-width: 2px;
+    padding-left: 2px;
+    padding-right: 2px;
+    vertical-align: middle;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/breadcrumbs.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,35 @@
+/*
+These styles are to do with the breadcrumbs that appear under the banner.
+*/
+
+#breadcrumbs-container {
+    border-bottom: 1px solid black;
+    background: #444444;
+    margin: 0;
+    padding: 0;
+    padding: 4px 32px;
+}
+
+#breadcrumbs-container ul {
+    padding: 0;
+    margin: 0;
+}
+
+#breadcrumbs-container ul li {
+    display: inline;
+    background: url('/img/breadcrumbseparator.svg') no-repeat;
+    background-position: 4px 2px;
+    padding: 0 0 0 18px;
+    color: white;
+    font-size: 12px;
+}
+
+#breadcrumbs-container ul li a {
+    color: white;
+}
+
+#breadcrumbs-container ul li:nth-child(1) {
+    display: inline;
+    background: none;
+    padding: 0;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/createuser.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,4 @@
+#create-user-captcha-response-input {
+    width: 64px;
+    display: inline;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/editpkgscreenshots.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,41 @@
+#pkg-screenshots-list {
+    margin-top: 8px;
+    margin-bottom: 2px;
+}
+
+#pkg-screenshots-list > div {
+    margin-top: 2px;
+    margin-bottom: 2px;
+    background-color: #EEE;
+}
+
+#pkg-screenshots-list > div .pkg-screenshot-image-container {
+    position: relative;
+    display: inline;
+}
+
+#pkg-screenshots-list > div .pkg-screenshot-image-container > img {
+    float: left;
+    border: 1px solid black;
+}
+
+#pkg-screenshots-list div.pkg-screenshot-controls {
+    margin-left: 190px;
+    padding-top: 4px;
+    padding-bottom: 4px;
+}
+
+#pkg-screenshots-list > div .pkg-screenshot-actions-container {
+    padding-top:2px;
+    padding-bottom:2px;
+    padding-left:4px;
+    padding-right:4px;
+ position: absolute; /* assumes that this is inside a 'pkg-screenshot-title' */
+    top: 1px;
+    right: 1px;
+    background-color: rgb(253,207,49);
+    border-bottom-left-radius: 4px;
+    display: inline;
+}
+
+
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/editpkgversionlocalization.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,77 @@
+#edit-pkg-version-localization-container {
+    position: relative;
+    border : 1px solid black;
+    border-collapse: collapse;
+    border: 1px black solid;
+    width: 100%
+}
+
+#edit-pkg-version-localization-container td {
+    padding-left: 20px;
+    padding-right: 20px;
+    padding-bottom: 20px;
+    padding-top: 20px;
+    vertical-align: top;
+}
+
+#edit-pkg-version-localization-container > tr > td {
+    border-top: 1px solid black;
+    border-bottom: 1px solid black;
+}
+
+#natural-translations-cell {
+    width: 16%;
+    border-right : 1px dotted lightgray;
+    border-left: 1px solid black;
+}
+
+#natural-translations-cell div {
+    padding: 4px;
+}
+
+#natural-translations-cell .selected-translation {
+    font-weight: bold;
+}
+
+#english-original-cell {
+    width: 30%;
+    border-left : 1px solid black;
+    border-right: 1px solid black;
+    background-color: #EEE;
+}
+
+#translation-cell {
+}
+
+#translation-cell-summary {
+    width: 100%;
+    height: 64px;
+}
+
+#translation-cell-description {
+    width: 100%;
+    height: 200px;
+}
+
+#english-original-cell h1 {
+    text-align: right;
+    margin-top: 2px;
+    margin-bottom: 2px;
+    font-size: 10px;
+    color: grey;
+}
+
+#english-original-cell h2 {
+    margin-top: 4px;
+    margin-bottom:2px;
+}
+
+#english-original-cell p {
+    margin-top: 2px;
+    margin-bottom:8px;
+}
+
+#edit-pkg-version-localization-actions-container {
+    margin-top: 16px;
+    margin-left: 16%
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/home.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,16 @@
+/*
+This specific layout works off the premise that there is only one table on this page.
+*/
+
+.home .table-general > tbody > tr > td:nth-child(1) {
+    width:18px;
+    text-align: center;
+}
+
+.home .table-general > tbody > tr > td:nth-child(3) {
+    width:160px;
+}
+
+.home .table-general > tbody > tr > td:nth-child(4) {
+    width:50%;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/listauthorizationpkgrules.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,4 @@
+.list-authorization-pkg-rules .table-general > tbody > tr > td:nth-child(4) {
+    width: 16px;
+    text-align: center;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/listrepositories.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,16 @@
+/*
+This specific layout works off the premise that there is only one table on this page.
+*/
+
+.list-repositories .table-general > tbody > tr > td:nth-child(1) {
+    width: 10%;
+    text-align: center;
+}
+
+.list-repositories .table-general > tbody > tr > td:nth-child(2) {
+    width:60%;
+}
+
+.list-repositories .table-general > tbody > tr > td:nth-child(3) {
+    width:30%;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/listusers.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,12 @@
+/*
+This specific layout works off the premise that there is only one table on this page.
+*/
+
+.list-users .table-general > tbody > tr > td:nth-child(1) {
+    width: 10%;
+    text-align: center;
+}
+
+.list-users .table-general > tbody > tr > td:nth-child(2) {
+    width:90%;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/main.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,534 @@
+/*
+These styles are applicable across a number of pages of the system.
+*/
+
+/*
+==================================
+GENERAL PAGE LAYOUT
+*/
+
+body {
+    font-family: sans-serif;
+    padding: 0;
+    margin: 0;
+}
+
+img {
+    border: none;
+}
+
+a, a:link, a:visited, a:active, a:hover {
+    text-decoration: underline;
+    color: blue;
+}
+
+h1 {
+    font-weight: bold;
+    font-size: 18px;
+}
+
+h2 {
+    font-weight: bold;
+    font-size: 16px;
+    color: #444;
+}
+
+.muted {
+    color: gray;
+}
+
+/*
+Used by the 'highlighted-text' directive.
+*/
+.highlighted {
+    /*background-color: yellow;*/
+    /*border: 1px solid rgba(215, 215, 0, 1);*/
+    background-color: #ffc8fe;
+    border: 1px solid rgb(255, 149, 255);
+}
+
+small {
+    font-size: 10px;
+}
+
+/*
+This class is used for the spinner as well as the modal dialog. It will fill a backdrop with a dark colour and then
+the spinner or the modal dialog can be shown over the top.
+*/
+
+.modal-backdrop-container {
+    position: fixed;
+    left: 0px;
+    top: 0px;
+    bottom: 0px;
+    right: 0px;
+    background-color: rgba(0, 0, 0, 0.6);
+    z-index: 1050;
+}
+
+/*
+==================================
+TEXT STYLING
+*/
+
+.text-error {
+    color: red;
+}
+
+.text-warning {
+    color: darkorange;
+}
+
+.text-success {
+    color: darkgreen;
+}
+
+/*
+==================================
+PAGE STRUCTURE
+*/
+
+.content-container {
+    /* handled by the trailer class */
+    margin: 24px 5% 0px;
+    width: auto;
+}
+
+.footer {
+    clear:both;
+    margin-bottom: 32px;
+}
+
+/*
+==================================
+ALERT BOXES FOR USE OUTSIDE OF FORMS
+*/
+
+.alert-container {
+    padding: 8px;
+    border: 1px solid red;
+    background-color: white;
+    color: red;
+    margin-bottom: 4px;
+    margin-top: 4px;
+}
+
+.info-container {
+    padding: 8px;
+    border: 1px solid #336698;
+    background-color: white;
+    color: black;
+    margin-bottom: 4px;
+    margin-top: 4px;
+}
+
+.onelineform-container {
+    padding: 8px;
+    border: 1px solid black;
+    background-color: white;
+    color: black;
+}
+
+.onelineform-container form {
+    margin-bottom: 0;
+}
+
+/*
+==================================
+FORMS
+*/
+
+label {
+    width: 30%;
+    display: inline-block;
+    text-align: right;
+    color: gray;
+    line-height: 100%;
+    vertical-align: top;
+    margin-top:5px;
+    font-weight: lighter;
+}
+
+input, textarea, select {
+    font-size: 16px;
+    border: 1px solid darkgray;
+    padding: 2px;
+}
+
+button {
+    background-color: white;
+    border: 1px solid black;
+    border-radius: 6px;
+    font-size: 16px;
+    padding-top: 2px;
+    padding-bottom: 2px;
+    padding-left: 8px;
+    padding-right: 8px;
+}
+
+button.main-action {
+    border: 2px solid black;
+}
+
+button[disabled] {
+    border: 1px solid darkgray;
+    color: rgba(0, 0, 0, 0.50);
+}
+
+button.main-action[disabled] {
+    border: 2px solid darkgray;
+}
+
+.form-error-container {
+    padding-top: 4px;
+    color: red;
+}
+
+.form-control-group {
+    width: 69%;
+    display: inline-block;
+    line-height: 100%;
+    padding-bottom: 8px;
+}
+
+/*
+In a form, you may want to place some static text as one of the 'controls'. This style helps with this.
+*/
+
+.form-control-group .form-control-group-static {
+    padding: 3px;
+}
+
+.form-control-group.form-control-group-error input {
+    border: 1px solid red;
+}
+
+/*
+This is used to place a box above the form in order to show some alert text such as "your login has failed."
+*/
+
+.form-alert-container {
+    width:69%;
+    margin-left: 30%;
+    margin-top: 16px;
+    margin-bottom: 16px;
+    padding: 8px;
+    border: 1px solid red;
+    background-color: white;
+    color: red;
+}
+
+.form-info-container {
+    width:69%;
+    margin-left: 30%;
+    margin-top: 16px;
+    margin-bottom: 16px;
+    padding: 8px;
+    border: 1px solid #336698;
+    background-color: white;
+    color: #336698;
+}
+
+/*
+This resides at the bottom of the form and contains the action buttons relating to the controls.
+*/
+
+.form-action-container {
+    width: 69%;
+    margin-left: 30%;
+    margin-top: 16px;
+    padding-top: 8px;
+    border-top: 1px solid black;
+}
+
+/*
+==================================
+TABLE
+*/
+
+table.table-general {
+    width: 100%;
+    border: 1px solid black;
+    border-collapse: collapse;
+    clear: both;
+}
+
+table.table-general thead th {
+    background: #444;
+    color: white;
+    text-align: left;
+    padding: 4px;
+}
+
+table.table-general tbody td {
+    padding: 4px;
+}
+
+table.table-general tbody td:nth-child(n+1) {
+    border-left: 1px dotted #444;
+}
+
+table.table-general tbody tr:nth-child(even) {
+    background: white;
+}
+table.table-general tbody tr:nth-child(odd) {
+    background: #EEE;
+}
+
+table.table-general tbody tr td.timestampcell {
+    width: 172px;
+}
+
+table.table-general tbody tr td.selectioncell {
+    width: 32px;
+    text-align: center;
+}
+
+.table-general-container {
+}
+
+.table-general-pagination-container {
+    float: right;
+    margin-top: 2px;
+    margin-bottom: 2px;
+}
+
+/*
+==================================
+ANGULARJS
+*/
+
+[ng\:cloak], [ng-cloak], .ng-cloak {
+    display: none;
+}
+
+/*
+==================================
+MODAL DIALOG
+Also see "modal-backdrop-container" class above.
+*/
+
+.modal-container {
+    width: 240px;
+    height: 320px;
+    border: 1px black solid;
+    background-color: white;
+    position: fixed;
+    left: 50%;
+    top: 50%;
+    margin-left: -120px;
+    margin-top: -160px;
+}
+
+.modal-container > .modal-content-container {
+    padding: 16px;
+    position: absolute;
+    top: 20px;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    overflow-y: auto;
+}
+
+.modal-container > .modal-banner-container {
+    background-color: #336698;
+    color: white;
+    height: 19px;
+    border-bottom: 1px solid rgba(0,0,0,0.6);
+    text-align: right;
+}
+
+/*
+This deals with the close icon in the banner.
+*/
+
+.modal-container > .modal-banner-container img {
+   margin-top: 3px;
+    margin-right: 3px;
+}
+
+/*
+==================================
+SPINNER
+*/
+
+/*
+This material is for the spinner; it will fill the screen will white and then put a small animation on top for
+a moment.
+*/
+
+#spinner-container > div {
+    width: 120px;
+    margin: 0 auto;
+    padding-top: 120px;
+}
+
+/*
+==================================
+SEARCH BAR
+*/
+
+#search-criteria-container {
+    background-color: rgb(253,207,49);
+    border: 1px solid black;
+}
+
+#search-criteria-container label {
+    font-size: 10px;
+    white-space: nowrap;
+    margin: 5px;
+    width: auto;
+}
+
+#search-criteria-container label::after {
+    content:":";
+}
+
+#search-criteria-container input {
+    font-size: 10px;
+}
+
+#search-criteria-container select {
+    font-size: 10px;
+}
+
+#search-criteria-container button {
+    font-size: 10px;
+    background-color: rgb(253,207,49);
+    border: 1px solid black;
+    border-radius: 3px;
+}
+
+#search-criteria-container button[disabled] {
+    border: 1px solid rgba(0, 0, 0, 0.30);
+}
+
+#search-criteria-container > div {
+    display: inline-block;
+    padding-top:2px;
+    padding-bottom:2px;
+    padding-left:4px;
+    padding-right:4px;
+}
+
+/*
+Within the search criteria to break-up the sections of the search criteria, this will put a dotted line
+between the sections.
+*/
+
+#search-criteria-container > div:nth-child(n+2) {
+    border-left: 1px dotted black;
+}
+
+#search-results-container {
+    margin-top: 20px;
+}
+
+/*
+==================================
+ACTIVE / INACTIVE INDICATORS -- SVG
+*/
+
+.active-indicator {
+    fill: firebrick;
+}
+
+.active-indicator.active-indicator-true {
+    fill: darkolivegreen;
+}
+
+/*
+==================================
+DATA LISTS
+*/
+
+dl dt {
+    color:gray;
+    float:left;
+    clear:left;
+    text-align: right;
+    margin-right:10px;
+    padding:5px;
+    width:190px;
+}
+
+dl dd {
+    margin:2px 0;
+    padding:5px 0;
+    margin-left: 200px;
+}
+
+/*
+==================================
+HIDE / SHOW FOR OWN DIRECTIVES
+*/
+
+.app-hide {
+    display: none !important;
+}
+
+/*
+==================================
+PAGINATION CONTROLS
+*/
+
+.pagination-control-container {
+    display: inline-block;
+    padding: 0;
+    margin: 0;
+}
+
+.pagination-control-container.pagination-control-on-first a.pagination-control-left > img {
+    opacity: 0.25;
+}
+
+.pagination-control-container.pagination-control-on-last a.pagination-control-right > img {
+    opacity: 0.25;
+}
+
+.pagination-control-container li {
+    min-width: 1.75em;
+    text-align: center;
+    display: inline-block;
+    padding: 0;
+}
+
+.pagination-control-container > li > a,svg {
+    margin-left: 4px;
+    margin-right: 4px;
+}
+
+.pagination-control-container > li > a.pagination-control-currentpage {
+    font-weight: bold;
+    text-decoration: none;
+    color: black;
+}
+
+/*
+==================================
+CONTEXT-MENU
+This is where the user is presented with a pop-up modal that contains some options to choose from.
+*/
+
+ul.context-menu-container {
+    padding-left: 0;
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+ul.context-menu-container li {
+    margin-top: 2px;
+    margin-bottom: 2px;
+    list-style: none;
+    background-color: #eeeeee;
+    padding: 4px;
+}
+
+/*
+==================================
+RATINGS
+*/
+
+.rating-indicator {
+    vertical-align: middle;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/pkgfeedbuilder.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,21 @@
+.pkg-feed-builder .feed-url-container {
+    font-family: "Courier New", Courier, mono;
+    padding: 4px 8px;
+    border: 1px solid black;
+    background-color: #EEE;
+}
+
+.pkg-feed-builder .pkg-lozenge-container {
+    padding-top: 3px;
+    padding-bottom: 3px;
+}
+
+.pkg-feed-builder .pkg-lozenge-container .pkg-lozenge {
+    background-color: #EEE;
+    border-radius: 4px;
+    padding: 2px 6px;
+    display: inline-block;
+    margin-left: 2px;
+    margin-right: 2px;
+    border: 1px solid lightgray;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/unsupported.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,42 @@
+/*
+This material refers to a panel that may appear if the browser is not supported or the javascript environment is
+not configured.
+*/
+
+#unsupported {
+    background-color: #505050;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    font-family: sans-serif;
+}
+
+#unsupported.unsupported-hide {
+    display: none;
+}
+
+#unsupported h1 {
+    text-align: center;
+}
+
+#unsupported .unsupported-image {
+    text-align: center;
+}
+
+#unsupported #unsupported-container {
+    color: white;
+    width: 420px;
+    height: 320px;
+    margin:0 auto;
+    margin-top: 72px;
+}
+
+#unsupported #unsupported-container .unsupported-message-container {
+    margin-bottom: 20px;
+}
+
+#unsupported #unsupported-container .unsupported-message-container > a {
+    color: white;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/singlepage/viewpkg.css Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,160 @@
+#pkg-title {
+    margin-bottom: 6px;
+}
+
+#pkg-title-feed {
+    float: right;
+}
+
+#pkg-title-icon {
+    display: inline;
+    float:left;
+}
+
+#pkg-title-text {
+    padding-left: 38px;
+}
+
+#pkg-title-text h1 {
+    margin: 0;
+}
+
+/*
+---------------------
+USER RATINGS
+*/
+
+#pkg-rating-indicator-container {
+    margin-top: 6px;
+    margin-bottom: 8px;
+}
+
+#pkg-rating-indicator-container .pkg-ratings-indicator-sample {
+    margin-left: 12px;
+    color: gray;
+}
+
+/*
+---------------------
+DESCRIPTION / META-DATA
+*/
+
+#pkg-metadata-container {
+    border-top: 1px lightgray solid;
+}
+
+#pkg-description-container {
+
+}
+
+/*
+---------------------
+USER-RATINGS
+*/
+
+#pkg-userratings-container {
+    border-top: 1px lightgray solid;
+}
+
+#pkg-userratings-container .pkg-userratings-pagination {
+    float: right;
+    margin-top: 2px;
+    margin-bottom: 2px;
+}
+
+#pkg-userratings-container .pkg-userrating {
+    display: inline-block;
+    width: 360px;
+    height: 82px;
+    padding-left: 4px;
+    padding-right: 4px;
+    padding-top: 2px;
+    padding-bottom: 2px;
+    margin-bottom: 8px;
+    margin-right: 8px;
+    vertical-align: top;
+    background-color: #f4f4f4;
+    border-left: 2px solid gray;
+    position: relative;
+}
+
+#pkg-userratings-container .pkg-userrating-indicatoranduser {
+    height: 26px;
+}
+
+#pkg-userratings-container .pkg-userrating-meta {
+    float: right;
+    font-size: 10px;
+}
+
+#pkg-userratings-container .pkg-userrating-comment {
+    /*clear: both;*/
+    font-size: 10px;
+    color: gray;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    height: 48px;
+}
+
+#pkg-userratings-container .pkg-userrating-differentversion {
+    position: absolute;
+    bottom: 0px;
+    right: 0px;
+    background-color: rgba(255,0,0,0.5);
+    font-size: 10px;
+    color: white;
+    padding-left: 4px;
+    padding-right: 4px;
+    padding-top: 2px;
+    padding-bottom: 2px;
+    border-top-left-radius: 3px;
+}
+
+/*
+---------------------
+ACTIONS
+*/
+
+#pkg-actions-container {
+    border-top: 1px lightgray solid;
+}
+
+/*
+---------------------
+SCREENSHOTS
+*/
+
+#pkg-screenshot-container {
+    width: 100%;
+    border: 1px solid black;
+    background-color: #444;
+    overflow: auto;
+    height: 256;
+    padding-left: 8px;
+    padding-right: 8px;
+    margin-top: 20px;
+}
+
+.pkg-screenshot-item-container {
+    margin: 8px;
+    box-shadow: 2px 2px 4px black;
+    display: inline-block;
+    position: relative;
+}
+
+.pkg-screenshot-item-controls-container a {
+    text-decoration: none;
+}
+
+.pkg-screenshot-item-controls-container {
+    padding-top:2px;
+    padding-bottom:2px;
+    padding-left:4px;
+    padding-right:4px;
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    background-color: rgb(253,207,49);
+    border-bottom-left-radius: 4px;
+    display: inline;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/img/paginationleft.png Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,16 @@
+‰PNG
+
+
+IHDR
+
+Vu\çsBIT|dˆ   pHYs
+×
+×B(›xtEXtSoftwarewww.inkscape.org›î<`IDAT(‘’»€0
+CŸ)%S±
+“žy
+Z4IA~¾Äwjt’Oþ ‰€
+ˆßàä€x
+
+)•
+=ƒIÂÌp§(e%ñ‹àakt
+Ö|¤Ù¡×׺|8ï5>Í îʨ›ìSIEND®B`‚
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/img/paginationleft.svg Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg";
+   version="1.1"
+   height="12"
+   width="12">
+ <path fill="black" d="M12 4.5 L12 7.5 L8 7.5 L8 12 L0 6 L8 0 L8 4.5"/>
+   </svg>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/img/paginationright.png Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,11 @@
+‰PNG
+
+
+IHDR
+
+Vu\çsBIT|dˆ   pHYs
+×
+×B(›xtEXtSoftwarewww.inkscape.org›î<oIDAT(‘­Ñ±
+ÂP
+Ðg”*£Ð3
+u؄°I–"›¤6‘Ï'èK9éÛw:Û2Ó'1¡/ëï~E¸ãØ"H,Z/®"v1ÚƀSDœ3s®9þâ‚Ëáû:܊ڵ27ã©uéÝÎÚô¸U„’£3ñQ‘,¿IEND®B`‚
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/img/paginationright.svg Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg";
+   version="1.1"
+   height="12"
+   width="12">
+      <path fill="black" d="M0 4.5 L0 7.5 L4 7.5 L4 12 L12 6 L4 0 L4 4.5"/>
+   </svg>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/img/starhalf.png Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,20 @@
+‰PNG
+
+
+IHDRóÿabKGDÿÿÿ ½§“     pHYs¯¯^‘
+tIMEÞ
+!94ìŒIDAT8Ë͒=KcA†Ÿ™äFw£˜Ør—-´ZÑÊmea+»H
+1ÅB‹­/þ±P®°
+
+¬Yÿ€ ‚ÜAdm²
+˜øŸ¹wŽMPÑ$"6žnΜyyŸw¼±T¥‹®ñŠù¯Ÿ×W'
+ªü¨®$™êEcûv¡¶ÒãªIëZÇ^ÐÝÿ““èMa´
+rV¼úpþwi ¼@gÊû„Ф„˜Ñ4(#u(õøÚ!¢,u
+¾     ňe~íyðgzÂWm#3|7ÞZÊPÚ õÜ*eäڊ_ýËDOÞ™
+Å¡BvÞiYªš@6ƒš£˜Áu2hK¸ì¤]:†çR
+ñž:@ëÐÿMoèôÅ;‡=y† ¢²
+ÎnÕ_H$V8ˆç¿kFl 
+P·aÿp{vô²â
+¤Ó½ßKÇ""=‘ɗø%⇣O
+„Ëô¿"’ëË,LšRo¯=å5‡ 8ª¾‰Â`vÞIžYMæ¾çºlÍ99ñý
+ï®î+á•EÒBgIEND®B`‚
=======================================
--- /dev/null   
+++ /haikudepotserver-webapp/src/main/webapp/img/staroff.png Sat Aug 30 10:29:45 2014 UTC
Binary file, no diff available.
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/img/staron.png Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,15 @@
+‰PNG
+
+
+IHDRóÿabKGDÿÿÿ ½§“     pHYs¯¯^‘
+tIMEÞ
+"ºÇâèAIDAT8Ë͒±NAEÏL‰  ±5‘ÊÄd­
+JÁƵØ]¿Àoð`„‚Êõ(
+ؚLbbgbkcD-¶ÙgÁ)v    Æƛ¼bæÝ{_æ΃?Be5ªç=>sÑ>Àf”{÷ÝTžÎ2÷]DðDð²Ä+
+\&õ»'ÔÏzL7¢„0a5‹_¹»û›nàxmeÇEѶ@]{ ïäҊ™‰ÖS„wkü7åx
+´–B
+óì®þ‹Ö
+Ʊšik|™YT€îâ®5Ae®ñ2p¼Öø8~ÛE¸ÉHìÂv‚Þ‚›¢ãµ%íޚ@­ü
+fó–×ÒäH!ã4AUw&¥‡0l¥ïA¶PH#9FÀqRÑ|š4–ÅY‹t
+
+¬      òåÚhX®†Öy`ôVãÀkס.¼Üû_øäMt*Ø8ÕIEND®B`‚
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java Sat Aug 30 10:29:45 2014 UTC
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.multipage.model;
+
+import org.fest.assertions.Assertions;
+import org.junit.Test;
+
+public class PaginationTest {
+
+    /**
+     * <p>Tests the special case where there's one page.</p>
+     */
+
+    @Test
+    public void testGenerateSuggestedPages_one() {
+        Pagination p = new Pagination(1,0,10);
+ Assertions.assertThat(p.generateSuggestedPages(6)).isEqualTo(new int[] {0});
+    }
+
+    @Test
+    public void testGenerateSuggestedPages_linear() {
+        Pagination p = new Pagination(50,23,10);
+ Assertions.assertThat(p.generateSuggestedPages(6)).isEqualTo(new int[] {0,1,2,3,4});
+    }
+
+    @Test
+    public void testGenerateSuggestedPages_fanRight() {
+        Pagination p = new Pagination(500,21,10);
+ Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,1,2,3,4,8,14,23,34,49});
+    }
+
+    @Test
+    public void testGenerateSuggestedPages_fanLeft() {
+        Pagination p = new Pagination(500,475,10);
+ Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,13,23,31,38,43,45,46,48,49});
+    }
+
+    @Test
+    public void testGenerateSuggestedPages_fanLeftAndRight() {
+        Pagination p = new Pagination(500,250,10);
+ Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,9,16,21,23,24,27,31,38,49});
+    }
+
+}
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/web/controller/EntryPointController.java Sat Jul 12 08:36:41 2014 UTC
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2014, Andrew Lindesay
- * Distributed under the terms of the MIT License.
- */
-
-package org.haikuos.haikudepotserver.web.controller;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-
-/**
- * <p>This controller renders the default HTML entry point into the application. As this is <em>generally</em> a - * single page application, this controller will just render that single page.</p>
- */
-
-@Controller
-@RequestMapping("/")
-public class EntryPointController {
-
-    @RequestMapping(method = RequestMethod.GET)
-    public String entryPoint() {
-        return "entryPoint";
-    }
-
-}
=======================================
***Additional files exist in this changeset.***

==============================================================================
Revision: 8dbcbd8a7f4b
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sun Aug 31 09:07:52 2014 UTC
Log: sort out problems with pagination links in java as well as javascript

https://code.google.com/p/haiku-depot-web-app/source/detail?r=8dbcbd8a7f4b

Modified:
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PaginationLinksTag.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/model/Pagination.java
 /haikudepotserver-webapp/src/main/webapp/WEB-INF/includes/unsupported.jsp
/haikudepotserver-webapp/src/main/webapp/js/app/directive/paginationcontroldirective.js /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java

=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PaginationLinksTag.java Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/markup/PaginationLinksTag.java Sun Aug 31 09:07:52 2014 UTC
@@ -98,7 +98,7 @@
                 "<--",
                 0==p.getPage() ? "" : deriveHref(p.getPage()-1));

- int[] pageNumbers = p.generateSuggestedPages(null==getLinkCount() ? LINK_COUNT_DEFAULT : getLinkCount()); + int[] pageNumbers = p.generateSuggestedPages(null == getLinkCount() ? LINK_COUNT_DEFAULT : getLinkCount());

         for(int pageNumber : pageNumbers) {
             tagWriter.startTag("li");
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/model/Pagination.java Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/multipage/model/Pagination.java Sun Aug 31 09:07:52 2014 UTC
@@ -7,6 +7,8 @@

 import com.google.common.base.Preconditions;

+import java.util.Arrays;
+
 /**
* <P>This object aims to provide the pagination within a list of items. It aims to be more or less
  * like the "paginationcontroldirective.js" behaviour.</P>
@@ -106,7 +108,7 @@

int page = getPage() - 1; // assume the actual page has been set already

-        for (int i = 0; i < startI; i++) {
+        for (int i = 0; i <= startI; i++) {
             float p = (float) i / (float) startI;
             float f = p * p;
             result[startI - i] = Math.min(
@@ -125,7 +127,7 @@

     public int[] generateSuggestedPages(int count) {

- Preconditions.checkState(count > 3 && 0==count%2,"the count of pages must be more than 3 and an even number"); + Preconditions.checkState(count > 3,"the count of pages must be more than 3");

         int pages = getPages();

@@ -142,6 +144,9 @@
         int[] result = new int[count];
         int middleI = count / 2;

+        // a debugging aid to see any bad values easily.
+        Arrays.fill(result,-10);
+
         if(page < middleI) {

             for(int i=0;i<=page;i++) {
@@ -155,7 +160,7 @@

             int remainder = pages - page;

-            if(remainder < middleI) {
+            if(remainder <= (result.length - middleI) - 1) {

                 for(int i=0;i<remainder;i++) {
                     result[result.length - (i + 1)] = (pages - 1) - i;
=======================================
--- /haikudepotserver-webapp/src/main/webapp/WEB-INF/includes/unsupported.jsp Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/includes/unsupported.jsp Sun Aug 31 09:07:52 2014 UTC
@@ -31,8 +31,9 @@
<a href="https://www.haiku-os.org/docs/userguide/en/applications/webpositive.html";>WebPositive</a>,
             <a href="https://www.mozilla.org/firefox";>Firefox</a>
und <a href="https://www.google.com/chrome/browser/";>Google Chrome</a> - funktionieren auf alle F&#xE4;lle. Eine einfache Darstellung ist auch
-            <a href="/multipage">zu verfügen</a>.
+            funktionieren auf alle F&#xE4;lle.  Eine
+            <a href="/multipage">einfache Darstellung</a>
+            steht auch zur Verfügung.
         </div>

     </div>
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/directive/paginationcontroldirective.js Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/paginationcontroldirective.js Sun Aug 31 09:07:52 2014 UTC
@@ -8,8 +8,8 @@
  */

 angular.module('haikudepotserver').directive('paginationControl',[
-    '$parse','constants',
-    function($parse,constants) {
+    '$parse',
+    function($parse) {
         return {
             restrict: 'E',
             link : function($scope,element,attributes) {
@@ -34,10 +34,6 @@
                     if(result < 3) {
throw Error('a link count of ' + result + ' is not possible for the pagination control - it must be >= 3');
                     }
-
-                    if(0 == result % 2) {
- throw Error('a link count of ' + result + ' is not possible for the pagination control - it must be an odd number');
-                    }

                     return result;
                 }
@@ -47,16 +43,174 @@
                 var maxExpression = attributes['max'];
                 var pageControlEs = [];

-                function adjustOffset(multiplier) {
+                /**
+ * <p>This will return an object containing the parameters of the pagination.</p>
+                 */
+
+                function parameters() {
+
                     var offset = $scope.$eval(offsetExpression);
                     var max = $scope.$eval(maxExpression);
                     var total = $scope.$eval(totalExpression);
-                    offset += multiplier * max;

-                    if(offset >= 0 && offset < total) {
-                        $parse(offsetExpression).assign($scope,offset);
+ // it is possible that those may evaluate to strings; in which case it is necessary to
+                    // parse those into numerical values.
+
+                    if(!angular.isNumber(offset)) {
+                        offset = parseInt(''+offset,10);
                     }
+
+                    if(!angular.isNumber(max)) {
+                        max = parseInt(''+max,10);
+                    }
+
+                    if(!angular.isNumber(total)) {
+                        total = parseInt(''+total,10);
+                    }
+
+                    if(max <= 0) {
+ throw Error('the \'max\' value must be a positive integer');
+                    }
+
+                    if(offset < 0) {
+                        throw Error('the \'offset\' must be >= 0');
+                    }
+
+                    if(0 != total && offset >= total) {
+                        throw Error('the \'offset\' must be < '+total);
+                    }
+
+ var pages = Math.floor((total / max) + (total % max ? 1 : 0));
+                    var page = Math.floor(offset / max); // current page.
+
+                    return {
+                        offset : offset,
+                        max : max,
+                        total : total,
+                        pages : pages,
+                        page : page
+                    };
+
                 }
+
+                // ---------------------
+                // NAVIGATION / ALGORITHMS
+
+                /**
+                 * <p>This is used to go back or forward a page.</p>
+ * @param direction -1 go back 1 page, 1 go forward one page.
+                 */
+
+                function pageJumpBackOrForward(direction) {
+                    var p = parameters();
+                    var offset = p.offset + (direction * max);
+
+                    if(offset >= 0 && offset < p.total) {
+                        $parse(offsetExpression).assign($scope, offset);
+                    }
+                }
+
+                /**
+ * <p>This generates the set of offsets to pages that are suggested for the pagination.</p>
+                 */
+
+                function generateSuggestedPages(params, count) {
+
+                    switch(params.pages) {
+
+                        case 0:
+                            return [];
+
+                        case 1:
+                            return [0];
+
+                        default:
+                            var result = [];
+
+                            if (params.pages <= count) {
+
+                                // linear fill
+
+                                for (var i = 0; i < params.pages; i++) {
+                                    result.push(i);
+                                }
+                            }
+                            else {
+
+ // fill the result with rubbish so that it is easy to detect problems.
+
+                                for (var j = 0; j < count; j++) {
+                                    result.push(-10);
+                                }
+
+                                function fanFillRight(startI) {
+
+                                    var pages = params.pages - 1;
+                                    var page = params.page + 1;
+                                    var len = count - startI;
+
+                                    for (var k = 0; k < len; k++) {
+                                        var p = k / (len - 1);
+                                        var f = p * p;
+                                        result[startI + k] = Math.max(
+ result[(startI + k) - 1] + 1, + page + Math.floor(f * (pages - page)));
+                                    }
+
+                                }
+
+                                function fanFillLeft(startI) {
+
+ var page = params.page - 1; // assume the actual page has been set already
+
+                                    for (var l = 0; l <= startI; l++) {
+                                        var p = l / startI;
+                                        var f = p * p;
+                                        result[startI - l] = Math.min(
+ result[(startI - l) + 1] - 1, + page - Math.floor(f * page));
+                                    }
+                                }
+
+                                var middleI = Math.floor(count / 2);
+
+                                if (params.page < middleI) {
+
+ for (var m = 0; m <= params.page; m++) {
+                                        result[m] = m;
+                                    }
+
+                                    fanFillRight(params.page + 1);
+
+                                }
+                                else {
+
+ var remainder = params.pages - params.page;
+
+ if (remainder <= (result.length - middleI) - 1) {
+
+ for (var n = 0; n < remainder; n++) { + result[result.length - (n + 1)] = (params.pages - 1) - n;
+                                        }
+
+ fanFillLeft(result.length - (remainder + 1));
+
+                                    }
+                                    else {
+                                        result[middleI] = params.page;
+                                        fanFillRight(middleI + 1);
+                                        fanFillLeft(middleI - 1);
+                                    }
+
+                                }
+                            }
+
+                            return result;
+                    }
+                }
+
+                // ---------------------
+                // DOM SETUP

                 // so we need this many elements to use as page numbers.

@@ -71,14 +225,14 @@
                 topLevelE.append(leftArrowListItemE);

                 leftArrowAnchorE.on('click', function(event) {
-                    $scope.$apply(function() { adjustOffset(-1); });
+ $scope.$apply(function() { pageJumpBackOrForward(-1); });
                     event.preventDefault();
                     return false;
                 });

                 for(var i=0;i<deriveLinkCount();i++) {
- var listItemE = angular.element('<li class=\"app-hide\"></li>'); - var pageControlE = angular.element('<a href=\"\"></a>'); + var listItemE = angular.element('<li class="app-hide"></li>');
+                    var pageControlE = angular.element('<a href=""></a>');
                     pageControlEs.push(pageControlE);
                     listItemE.append(pageControlE);
                     topLevelE.append(listItemE);
@@ -111,196 +265,65 @@
                 topLevelE.append(rightArrowListItemE);

                 rightArrowAnchorE.on('click', function(event) {
-                    $scope.$apply(function() { adjustOffset(1); });
+ $scope.$apply(function() { pageJumpBackOrForward(1); });
                     event.preventDefault();
                     return false;
                 });

-                function disableAllPageControls() {
-                    for(var i=0;i<pageControlEs.length;i++) {
-                        pageControlEs[i].html('');
-                        pageControlEs[i].parent().addClass('app-hide');
-                        pageControlEs[i].attr('pagination-offset','');
-                    }
-                }
-
-                function getOffsetFromPageControlEAtIndex(i) {
-                    var a = pageControlEs[i].attr('pagination-offset');
-
-                    if(a && a.length) {
-                        return parseInt(''+a,10);
-                    }
-
-                    return -1;
-                }
-
-                function refreshPageControlsWithValues(total,offset,max) {
-
-                    if(max <= 0) {
- throw Error('the \'max\' value must be a positive integer');
-                    }
-
-                    if(offset < 0) {
-                        throw Error('the \'offset\' must be >= 0');
-                    }
-
-                    if(offset >= total) {
-                        throw Error('the \'offset\' must be < '+total);
-                    }
+                // ---------------------
+                // REFERESH DATA

- var pages = Math.floor((total / max) + (total % max ? 1 : 0));
-                    var page = Math.floor(offset / max); // current page.
+                function refreshPageControls() {
+                    var params = parameters();

// if we're on the first or the last pages when we will need to add a class to the pagination // controls so that we can control the appearance of the controls.

-                    if(0==page) {
+                    if(0==params.total || 0==params.page) {
                         topLevelE.addClass('pagination-control-on-first');
                     }
                     else {
topLevelE.removeClass('pagination-control-on-first');
                     }

-                    if(page==pages-1) {
+                    if(0==params.total || params.page==params.pages-1) {
                         topLevelE.addClass('pagination-control-on-last');
                     }
                     else {
topLevelE.removeClass('pagination-control-on-last');
                     }

-                    function setPageControl(i,pageNumber) {
-                        if(null==pageNumber) {
-                            pageControlEs[i].text('');
+                    // now render the pages in between.
+
+ var suggestedPages = generateSuggestedPages(params, pageControlEs.length);
+
+                    for(var i=0;i<pageControlEs.length;i++) {
+
+                        if(i >= suggestedPages.length) {
                             pageControlEs[i].parent().addClass('app-hide');
pageControlEs[i].removeClass('pagination-control-currentpage');
                             pageControlEs[i].attr('pagination-offset','');
                         }
                         else {
-                            pageControlEs[i].text('' + (pageNumber + 1));
pageControlEs[i].parent().removeClass('app-hide'); - pageControlEs[i].attr('pagination-offset',''+pageNumber * max);

-                            if(pageNumber==page) {
+                            if(params.page == suggestedPages[i]) {
pageControlEs[i].addClass('pagination-control-currentpage');
                             }
                             else {
pageControlEs[i].removeClass('pagination-control-currentpage');
                             }
-                        }
-                    }
-
- function linearFillPageControl(startI,length,pageStart) {
-                        for(var i=0;i<length;i++) {
-                            setPageControl(startI+i,pageStart+i);
-                        }
-                    }
-
-                    if(pages <= 1) {
-                        disableAllPageControls();
-                    }
-                    else {
-
- // this function with p=[0,1] should give a nice curve that passes through 0 when p=0
-                        // and passes through 1 when p=1
-
-                        function ramp(p) {
-                            return p*p;
-                        }
-
- function fanFillRightPageControl(pageControlEsStartI) {
-
- var pageControlEsFillLength = (pageControlEs.length - pageControlEsStartI);
-
-                            for(var i=0;i<pageControlEsFillLength;i++) {
-                                var p = i/(pageControlEsFillLength-1);
-                                var f = ramp(p);
- var maxPagesRightOfPage = (pages - (page + 1))-1; - var nextPage = Math.floor((page + 1) + (maxPagesRightOfPage * f)); - var lastPage = Math.floor(getOffsetFromPageControlEAtIndex(pageControlEsStartI+i-1) / max);
-                                nextPage = _.max([nextPage,lastPage+1]);
- setPageControl(pageControlEsStartI + i,nextPage);
-                            }
-                        }
-
- function fanFillLeftPageControl(pageControlEsStartI) {
-
- var pageControlEsFillLength = pageControlEsStartI + 1;
-
-                            for(var i=0;i<pageControlEsFillLength;i++) {
-                                var p = i/(pageControlEsFillLength-1);
-                                var f = ramp(p);
- var nextPage = Math.floor((page - 1) - ((page - 1) * f)); - var lastPage = Math.floor(getOffsetFromPageControlEAtIndex(pageControlEsStartI-i+1) / max);
-                                nextPage = _.min([nextPage,lastPage-1]);
- setPageControl(pageControlEsStartI - i,nextPage);
-                            }
-                        }
-
- // if there are <= pages than controls then just show the pages linearly and hide the rest of
-                        // the controls that are not necessary.
-
-                        if(pages <= pageControlEs.length) {
-                            linearFillPageControl(0,pages,0);

-                            for(var i=pages;i<pageControlEs.length;i++) {
-                                setPageControl(i,null);
-                            }
+ pageControlEs[i].attr('pagination-offset',''+(suggestedPages[i] * params.max)); + pageControlEs[i].text('' + (suggestedPages[i] + 1));
                         }
-                        else {
-
- // we have more pages out there than we have page controls so, we need to put the first - // and last page into place, choose a sensible location for the current page and arrange
-                            // other sensible options for other pages.
-
- var middleI = Math.floor(pageControlEs.length / 2);
-
-                            if(page < middleI) { // close to the left side
-                                linearFillPageControl(0,page+1,0);
-                                fanFillRightPageControl(page+1);
-                            }
-                            else {
-                                var remainder = pages-page;

- if(remainder < middleI) { // close to the right side. - linearFillPageControl(pageControlEs.length - remainder,remainder,page); - fanFillLeftPageControl((pageControlEs.length - remainder) - 1);
-                                }
-                                else {
-                                    setPageControl(middleI,page);
-                                    fanFillRightPageControl(middleI+1);
-                                    fanFillLeftPageControl(middleI-1);
-                                }
-                            }
-                        }
                     }
-                }
-
- // looks at the settings bound to this and will show/hide the page numbers.
-
-                function refreshPageControls() {
-
- if(totalExpression && offsetExpression && maxExpression) {
-
-                        var total = $scope.$eval(totalExpression);
-                        var offset = $scope.$eval(offsetExpression);
-                        var max = $scope.$eval(maxExpression);

-                        if(!angular.isUndefined(total) &&
-                            !angular.isUndefined(offset) &&
-                            !angular.isUndefined(max) &&
-                            total > 0) {
- refreshPageControlsWithValues(total,offset,max);
-                        }
-                        else {
-                            disableAllPageControls();
-                        }
-                    }
-                    else {
-                        disableAllPageControls();
-                    }
                 }

-                refreshPageControls();
+                // ---------------------
+                // EVENT OBSERVATION

                 if(totalExpression) {
                     $scope.$watch(totalExpression, function () {
@@ -319,6 +342,8 @@
                         refreshPageControls();
                     });
                 }
+
+                refreshPageControls();

             }
         }
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java Sun Aug 31 09:07:52 2014 UTC
@@ -43,5 +43,17 @@
         Pagination p = new Pagination(500,250,10);
Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,9,16,21,23,24,27,31,38,49});
     }
+
+    @Test
+    public void testGenerateSuggestedPages_general_1() {
+        Pagination p = new Pagination(168,120,15);
+ Assertions.assertThat(p.generateSuggestedPages(9)).isEqualTo(new int[] {0,4,5,6,7,8,9,10,11});
+    }
+
+    @Test
+    public void testGenerateSuggestedPages_general_2() {
+        Pagination p = new Pagination(168,75,15);
+ Assertions.assertThat(p.generateSuggestedPages(9)).isEqualTo(new int[] {0,2,3,4,5,6,7,8,11});
+    }

 }

==============================================================================
Revision: ccc92b6dfc59
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sun Aug 31 10:18:17 2014 UTC
Log:      ability to get localized names back for some reference data
various small fixes and tweaks

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

Added:
/haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.17__Categories_code_lowercase.sql
Modified:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllNaturalLanguagesRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllPkgCategoriesRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllUserRatingStabilitiesRequest.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/NaturalLanguage.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/UserRatingStability.java
 /haikudepotserver-webapp/src/main/resources/messages_de.properties
/haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java

=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.17__Categories_code_lowercase.sql Sun Aug 31 10:18:17 2014 UTC
@@ -0,0 +1,5 @@
+-- ------------------------------------------------------
+-- PKG CATEGORIES' CODES LOWER CASE
+-- ------------------------------------------------------
+
+UPDATE haikudepot.pkg_category SET code=LOWER(code);
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java Thu Aug 14 11:03:33 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java Sun Aug 31 10:18:17 2014 UTC
@@ -13,13 +13,15 @@
 public interface MiscellaneousApi {

     /**
-     * <p>Returns a list of all of the categories.</p>
+ * <p>Returns a list of all of the categories. If a natural language code is supplied in the reuqest, then + * the results' names will be localized; otherwise a database-based default will be returned.</p>
      */

GetAllPkgCategoriesResult getAllPkgCategories(GetAllPkgCategoriesRequest getAllPkgCategoriesRequest);

     /**
-     * <p>Returns a list of all of the natural languages.</p>
+ * <p>Returns a list of all of the natural languages. If a natural language code is supplied in the request + * then the results' names will be localized; otherwise a database-based default will be returned.</p>
      */

GetAllNaturalLanguagesResult getAllNaturalLanguages(GetAllNaturalLanguagesRequest getAllNaturalLanguagesRequest);
@@ -55,7 +57,8 @@

     /**
* <p>This method will return all of the possible user rating stabilities that can be used when the user
-     * rates a package version.</p>
+ * rates a package version. If a natural language code is supplied in the request then the results' names + * will be localized; otherwise a database-based default will be used.</p>
      */

GetAllUserRatingStabilitiesResult getAllUserRatingStabilities(GetAllUserRatingStabilitiesRequest getAllUserRatingStabilitiesRequest);
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllNaturalLanguagesRequest.java Mon Mar 10 10:18:38 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllNaturalLanguagesRequest.java Sun Aug 31 10:18:17 2014 UTC
@@ -6,4 +6,11 @@
 package org.haikuos.haikudepotserver.api1.model.miscellaneous;

 public class GetAllNaturalLanguagesRequest {
+
+    /**
+ * <p>If supplied, the results' names will be localized by this natural language code.</p>
+     */
+
+    public String naturalLanguageCode;
+
 }
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllPkgCategoriesRequest.java Mon Mar 10 10:18:38 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllPkgCategoriesRequest.java Sun Aug 31 10:18:17 2014 UTC
@@ -6,4 +6,11 @@
 package org.haikuos.haikudepotserver.api1.model.miscellaneous;

 public class GetAllPkgCategoriesRequest {
+
+    /**
+ * <p>If supplied, the results' names will be localized by this natural language code.</p>
+     */
+
+    public String naturalLanguageCode;
+
 }
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllUserRatingStabilitiesRequest.java Mon Apr 21 10:48:33 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllUserRatingStabilitiesRequest.java Sun Aug 31 10:18:17 2014 UTC
@@ -6,4 +6,11 @@
 package org.haikuos.haikudepotserver.api1.model.miscellaneous;

 public class GetAllUserRatingStabilitiesRequest {
+
+    /**
+ * <p>If supplied, the results' names will be localized by this natural language code.</p>
+     */
+
+    public String naturalLanguageCode;
+
 }
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java Thu Aug 14 11:03:33 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java Sun Aug 31 10:18:17 2014 UTC
@@ -20,6 +20,7 @@
 import org.haikuos.haikudepotserver.support.RuntimeInformationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.context.MessageSource;
 import org.springframework.stereotype.Component;

 import javax.annotation.Resource;
@@ -47,20 +48,39 @@
     @Resource
     FeedOrchestrationService feedOrchestrationService;

+    @Resource
+    MessageSource messageSource;
+
     @Override
public GetAllPkgCategoriesResult getAllPkgCategories(GetAllPkgCategoriesRequest getAllPkgCategoriesRequest) {
         Preconditions.checkNotNull(getAllPkgCategoriesRequest);
         final ObjectContext context = serverRuntime.getContext();

+        final Optional<NaturalLanguage> naturalLanguageOptional =
+ Strings.isNullOrEmpty(getAllPkgCategoriesRequest.naturalLanguageCode)
+                ? Optional.<NaturalLanguage>absent()
+ : NaturalLanguage.getByCode(context, getAllPkgCategoriesRequest.naturalLanguageCode);
+
         return new GetAllPkgCategoriesResult(
                 Lists.transform(
                         PkgCategory.getAll(context),
new Function<PkgCategory, GetAllPkgCategoriesResult.PkgCategory>() {
                             @Override
public GetAllPkgCategoriesResult.PkgCategory apply(PkgCategory input) { - return new GetAllPkgCategoriesResult.PkgCategory(
-                                        input.getCode(),
-                                        input.getName());
+
+                                if(naturalLanguageOptional.isPresent()) {
+ return new GetAllPkgCategoriesResult.PkgCategory(
+                                            input.getCode(),
+                                            messageSource.getMessage(
+                                                    input.getTitleKey(),
+                                                    null, // params
+ naturalLanguageOptional.get().toLocale()));
+                                }
+                                else {
+ return new GetAllPkgCategoriesResult.PkgCategory(
+                                            input.getCode(),
+                                            input.getName());
+                                }
                             }
                         }
                 )
@@ -72,15 +92,31 @@
         Preconditions.checkNotNull(getAllNaturalLanguagesRequest);
         final ObjectContext context = serverRuntime.getContext();

+        final Optional<NaturalLanguage> naturalLanguageOptional =
+ Strings.isNullOrEmpty(getAllNaturalLanguagesRequest.naturalLanguageCode)
+                        ? Optional.<NaturalLanguage>absent()
+ : NaturalLanguage.getByCode(context, getAllNaturalLanguagesRequest.naturalLanguageCode);
+
         return new GetAllNaturalLanguagesResult(
                 Lists.transform(
                         NaturalLanguage.getAll(context),
new Function<NaturalLanguage, GetAllNaturalLanguagesResult.NaturalLanguage>() {
                             @Override
public GetAllNaturalLanguagesResult.NaturalLanguage apply(NaturalLanguage input) { - return new GetAllNaturalLanguagesResult.NaturalLanguage(
-                                        input.getCode(),
-                                        input.getName());
+
+                                if(naturalLanguageOptional.isPresent()) {
+ return new GetAllNaturalLanguagesResult.NaturalLanguage(
+                                            input.getCode(),
+                                            messageSource.getMessage(
+                                                    input.getTitleKey(),
+                                                    null, // params
+ naturalLanguageOptional.get().toLocale()));
+                                }
+                                else {
+ return new GetAllNaturalLanguagesResult.NaturalLanguage(
+                                            input.getCode(),
+                                            input.getName());
+                                }
                             }
                         }
                 )
@@ -213,15 +249,31 @@
         Preconditions.checkNotNull(getAllUserRatingStabilitiesRequest);
         final ObjectContext context = serverRuntime.getContext();

+        final Optional<NaturalLanguage> naturalLanguageOptional =
+ Strings.isNullOrEmpty(getAllUserRatingStabilitiesRequest.naturalLanguageCode)
+                        ? Optional.<NaturalLanguage>absent()
+ : NaturalLanguage.getByCode(context, getAllUserRatingStabilitiesRequest.naturalLanguageCode);
+
         return new GetAllUserRatingStabilitiesResult(
                 Lists.transform(
                         UserRatingStability.getAll(context),
new Function<UserRatingStability, GetAllUserRatingStabilitiesResult.UserRatingStability>() {
                             @Override
public GetAllUserRatingStabilitiesResult.UserRatingStability apply(UserRatingStability input) { - return new GetAllUserRatingStabilitiesResult.UserRatingStability(
-                                        input.getCode(),
-                                        input.getName());
+
+                                if(naturalLanguageOptional.isPresent()) {
+ return new GetAllUserRatingStabilitiesResult.UserRatingStability(
+                                            input.getCode(),
+                                            messageSource.getMessage(
+                                                    input.getTitleKey(),
+                                                    null, // params
+ naturalLanguageOptional.get().toLocale()));
+                                }
+                                else {
+ return new GetAllUserRatingStabilitiesResult.UserRatingStability(
+                                            input.getCode(),
+                                            input.getName());
+                                }
                             }
                         }
                 )
@@ -239,7 +291,9 @@
new Function<Prominence, GetAllProminencesResult.Prominence>() {
                             @Override
public GetAllProminencesResult.Prominence apply(Prominence input) { - return new GetAllProminencesResult.Prominence(input.getOrdering(), input.getName()); + return new GetAllProminencesResult.Prominence(
+                                            input.getOrdering(),
+                                            input.getName());
                             }
                         }
                 )
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/NaturalLanguage.java Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/NaturalLanguage.java Sun Aug 31 10:18:17 2014 UTC
@@ -55,6 +55,14 @@
ExpressionFactory.matchExp(MediaType.CODE_PROPERTY, code))),
                 null));
     }
+
+    /**
+ * <p>Can be used to lookup the title of this language in the localization strings.</p>
+     */
+
+    public String getTitleKey() {
+        return String.format("naturalLanguage.%s",getCode().toLowerCase());
+    }

     public Locale toLocale() {
         return Locale.forLanguageTag(getCode());
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/UserRatingStability.java Mon Apr 21 10:48:33 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/UserRatingStability.java Sun Aug 31 10:18:17 2014 UTC
@@ -44,5 +44,10 @@
query.addOrdering(new Ordering(NAME_PROPERTY, SortOrder.ASCENDING));
         return (List<UserRatingStability>) context.performQuery(query);
     }
+
+
+    public String getTitleKey() {
+        return String.format("userRatingStability.%s.title", getCode());
+    }

 }
=======================================
--- /haikudepotserver-webapp/src/main/resources/messages_de.properties Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/messages_de.properties Sun Aug 31 10:18:17 2014 UTC
@@ -378,7 +378,7 @@

 # Multipage (non-AngularJS) Interface
 multipage.banner.title.suffix=Einfach
-multipage.banner.note=Dies ist die vereinfachte Darstellung, gedacht für Minimal-Browser. +multipage.banner.note=Dies ist die vereinfachte Darstellung, gedacht für Minimal-Browser
 multipage.banner.note.full=Standard-Darstellung

 # Test case
=======================================
--- /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/views/multipage/viewPkgVersion.jsp Sun Aug 31 10:18:17 2014 UTC
@@ -39,7 +39,7 @@
<multipage:pkgIcon pkgVersion="${data.pkgVersion}" size="32"/>
             </div>
             <div id="pkg-title-text">
- <h1><c:out value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage).summary}"/></h1> + <h1><c:out value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage.code).summary}"/></h1>
                 <div class="muted">
                     <small>
                         <c:out value="${data.pkgVersion.pkg.name}"></c:out>
@@ -68,7 +68,7 @@

         <div id="pkg-description-container">
             <p>
- <multipage:plainTextContent value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage).description}"></multipage:plainTextContent> + <multipage:plainTextContent value="${data.pkgVersion.getPkgVersionLocalizationOrFallback(data.currentNaturalLanguage.code).description}"></multipage:plainTextContent>
             </p>
         </div>

=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java Thu Aug 14 11:03:33 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java Sun Aug 31 10:18:17 2014 UTC
@@ -19,6 +19,7 @@
 import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException;
 import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
 import org.haikuos.haikudepotserver.dataobjects.PkgCategory;
+import org.haikuos.haikudepotserver.dataobjects.UserRatingStability;
 import org.haikuos.haikudepotserver.feed.controller.FeedController;
 import org.haikuos.haikudepotserver.support.RuntimeInformationService;
 import org.junit.Test;
@@ -36,6 +37,53 @@

     @Resource
     RuntimeInformationService runtimeInformationService;
+
+    @Test
+    public void testGetAllUserRatingStabilities() {
+
+        // ------------------------------------
+ GetAllUserRatingStabilitiesResult result = miscellaneousApi.getAllUserRatingStabilities(new GetAllUserRatingStabilitiesRequest());
+        // ------------------------------------
+
+        ObjectContext objectContext = serverRuntime.getContext();
+
+ List<UserRatingStability> userRatingStabilities = UserRatingStability.getAll(objectContext);
+
+ Assertions.assertThat(userRatingStabilities.size()).isEqualTo(result.userRatingStabilities.size());
+
+        for (int i = 0; i < userRatingStabilities.size(); i++) {
+ UserRatingStability userRatingStability = userRatingStabilities.get(i); + GetAllUserRatingStabilitiesResult.UserRatingStability apiUserRatingStability = result.userRatingStabilities.get(i); + Assertions.assertThat(userRatingStability.getCode()).isEqualTo(apiUserRatingStability.code); + Assertions.assertThat(userRatingStability.getName()).isEqualTo(apiUserRatingStability.name);
+        }
+    }
+
+    @Test
+    public void testGetAllUserRatingStabilities_de() {
+
+ GetAllUserRatingStabilitiesRequest request = new GetAllUserRatingStabilitiesRequest();
+        request.naturalLanguageCode = NaturalLanguage.CODE_GERMAN;
+
+        // ------------------------------------
+ GetAllUserRatingStabilitiesResult result = miscellaneousApi.getAllUserRatingStabilities(request);
+        // ------------------------------------
+
+ Optional<GetAllUserRatingStabilitiesResult.UserRatingStability> userRatingStabilityOptional =
+                Iterables.tryFind(
+                        result.userRatingStabilities,
+ new Predicate<GetAllUserRatingStabilitiesResult.UserRatingStability>() {
+                            @Override
+ public boolean apply(GetAllUserRatingStabilitiesResult.UserRatingStability input) {
+                                return input.code.equals("mostlystable");
+                            }
+                        }
+                );
+
+ Assertions.assertThat(userRatingStabilityOptional.isPresent()).isTrue(); + Assertions.assertThat(userRatingStabilityOptional.get().name).isEqualTo("Ziemlich stabil");
+
+    }

     @Test
     public void testGetAllPkgCategories() {
@@ -57,6 +105,37 @@
Assertions.assertThat(pkgCategory.getCode()).isEqualTo(apiPkgCategory.code);
         }
     }
+
+    /**
+ * <p>If the client asks for all of the categories with a natural language code then they will be returned + * the localized name of the category where possible. This tests this with German as German translations
+     * are known to be present.</p>
+     */
+
+    @Test
+    public void testGetAllPkgCategories_de() {
+
+ GetAllPkgCategoriesRequest request = new GetAllPkgCategoriesRequest();
+        request.naturalLanguageCode = NaturalLanguage.CODE_GERMAN;
+
+        // ------------------------------------
+ GetAllPkgCategoriesResult result = miscellaneousApi.getAllPkgCategories(request);
+        // ------------------------------------
+
+ Optional<GetAllPkgCategoriesResult.PkgCategory> pkgCategoryOptional =
+                Iterables.tryFind(
+                        result.pkgCategories,
+ new Predicate<GetAllPkgCategoriesResult.PkgCategory>() {
+                            @Override
+ public boolean apply(GetAllPkgCategoriesResult.PkgCategory input) {
+                                return input.code.equals("EDUCATION");
+                            }
+                        }
+                );
+
+        Assertions.assertThat(pkgCategoryOptional.isPresent()).isTrue();
+ Assertions.assertThat(pkgCategoryOptional.get().name).isEqualTo("Ausbildung");
+    }

     @Test
     public void testGetAllNaturalLanguages() {
@@ -78,6 +157,37 @@
Assertions.assertThat(naturalLanguage.getCode()).isEqualTo(apiNaturalLanguage.code);
         }
     }
+
+    /**
+ * <p>It is possible to request the natural languages with a natural language code. In this case, the + * results will localize the names as opposed to using those onces directly from the database.</p>
+     */
+
+    @Test
+    public void testGetAllNaturalLanguages_de() {
+
+ GetAllNaturalLanguagesRequest request = new GetAllNaturalLanguagesRequest();
+        request.naturalLanguageCode = NaturalLanguage.CODE_GERMAN;
+
+        // ------------------------------------
+ GetAllNaturalLanguagesResult result = miscellaneousApi.getAllNaturalLanguages(request);
+        // ------------------------------------
+
+ Optional<GetAllNaturalLanguagesResult.NaturalLanguage> naturalLanguageOptional =
+                Iterables.tryFind(
+                        result.naturalLanguages,
+ new Predicate<GetAllNaturalLanguagesResult.NaturalLanguage>() {
+                            @Override
+ public boolean apply(GetAllNaturalLanguagesResult.NaturalLanguage input) {
+                                return input.code.equalsIgnoreCase("es");
+                            }
+                        }
+                );
+
+ Assertions.assertThat(naturalLanguageOptional.isPresent()).isTrue(); + Assertions.assertThat(naturalLanguageOptional.get().name).isEqualTo("Espa\u00F1ol");
+
+    }

     @Test
     public void getRuntimeInformation_asUnauthenticated() {
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java Sun Aug 31 09:07:52 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/multipage/model/PaginationTest.java Sun Aug 31 10:18:17 2014 UTC
@@ -35,13 +35,13 @@
     @Test
     public void testGenerateSuggestedPages_fanLeft() {
         Pagination p = new Pagination(500,475,10);
- Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,13,23,31,38,43,45,46,48,49}); + Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,15,26,35,41,45,46,47,48,49});
     }

     @Test
     public void testGenerateSuggestedPages_fanLeftAndRight() {
         Pagination p = new Pagination(500,250,10);
- Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,9,16,21,23,24,27,31,38,49}); + Assertions.assertThat(p.generateSuggestedPages(10)).isEqualTo(new int[] {0,11,18,23,24,25,26,28,36,49});
     }

     @Test

==============================================================================
Revision: 6dcf4d936436
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Sun Aug 31 12:05:02 2014 UTC
Log:      updates to integration tests for lower case pkg category names
update integration tests to show that bulk pkg get will return 'any' architecture packages

https://code.google.com/p/haiku-depot-web-app/source/detail?r=6dcf4d936436

Modified:
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/IntegrationTestSupportService.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/PkgApiIT.java

=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java Sat Aug 30 10:29:45 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java Sun Aug 31 12:05:02 2014 UTC
@@ -183,6 +183,10 @@
         if(!pkgVersionLocalizationOptional.isPresent()) {
pkgVersionLocalizationOptional = getPkgVersionLocalization(NaturalLanguage.CODE_ENGLISH);
         }
+
+        if(!pkgVersionLocalizationOptional.isPresent()) {
+ throw new IllegalStateException("unable to find the fallback localization for " + toString());
+        }

         return pkgVersionLocalizationOptional.get();
     }
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/IntegrationTestSupportService.java Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/IntegrationTestSupportService.java Sun Aug 31 12:05:02 2014 UTC
@@ -111,6 +111,14 @@
         addPngPkgIcon(objectContext, pkg, 32);
         addHvifPkgIcon(objectContext, pkg);
     }
+
+ public void addDummyLocalization(ObjectContext context, PkgVersion pkgVersion) { + PkgVersionLocalization pkgVersionLocalization = context.newObject(PkgVersionLocalization.class); + pkgVersionLocalization.setNaturalLanguage(NaturalLanguage.getByCode(context, NaturalLanguage.CODE_ENGLISH).get());
+        pkgVersionLocalization.setDescription("sample description");
+        pkgVersionLocalization.setSummary("sample summary");
+ pkgVersion.addToManyTarget(PkgVersion.PKG_VERSION_LOCALIZATIONS_PROPERTY, pkgVersionLocalization, true);
+    }

     public StandardTestData createStandardTestData() {

@@ -123,6 +131,7 @@

         Architecture x86 = Architecture.getByCode(context, "x86").get();
Architecture x86_gcc2 = Architecture.getByCode(context, "x86_gcc2").get();
+        Architecture any = Architecture.getByCode(context, "any").get();

         result.repository = context.newObject(Repository.class);
         result.repository.setActive(Boolean.TRUE);
@@ -140,7 +149,7 @@
         {
PkgPkgCategory pkgPkgCategory = context.newObject(PkgPkgCategory.class); result.pkg1.addToManyTarget(Pkg.PKG_PKG_CATEGORIES_PROPERTY, pkgPkgCategory, true); - pkgPkgCategory.setPkgCategory(PkgCategory.getByCode(context, "GRAPHICS").get()); + pkgPkgCategory.setPkgCategory(PkgCategory.getByCode(context, "graphics").get());
         }

         addPkgScreenshot(context,result.pkg1);
@@ -157,6 +166,7 @@
         result.pkg1Version1x86.setIsLatest(false);
         result.pkg1Version1x86.setPkg(result.pkg1);
         result.pkg1Version1x86.setRepository(result.repository);
+        addDummyLocalization(context, result.pkg1Version1x86);

         result.pkg1Version2x86 = context.newObject(PkgVersion.class);
         result.pkg1Version2x86.setActive(Boolean.TRUE);
@@ -218,6 +228,7 @@
         result.pkg2Version1.setIsLatest(true);
         result.pkg2Version1.setPkg(result.pkg2);
         result.pkg2Version1.setRepository(result.repository);
+        addDummyLocalization(context, result.pkg2Version1);

         result.pkg3 = context.newObject(Pkg.class);
         result.pkg3.setActive(true);
@@ -233,6 +244,23 @@
         result.pkg3Version1.setIsLatest(true);
         result.pkg3Version1.setPkg(result.pkg3);
         result.pkg3Version1.setRepository(result.repository);
+        addDummyLocalization(context, result.pkg3Version1);
+
+        result.pkgAny = context.newObject(Pkg.class);
+        result.pkgAny.setActive(true);
+        result.pkgAny.setName("pkgany");
+        result.pkgAny.setProminence(prominence);
+
+        result.pkgAnyVersion1 = context.newObject(PkgVersion.class);
+        result.pkgAnyVersion1.setActive(Boolean.TRUE);
+        result.pkgAnyVersion1.setArchitecture(any);
+        result.pkgAnyVersion1.setMajor("123");
+        result.pkgAnyVersion1.setMicro("123");
+        result.pkgAnyVersion1.setRevision(3);
+        result.pkgAnyVersion1.setIsLatest(true);
+        result.pkgAnyVersion1.setPkg(result.pkgAny);
+        result.pkgAnyVersion1.setRepository(result.repository);
+        addDummyLocalization(context, result.pkgAnyVersion1);

         context.commitChanges();

@@ -330,6 +358,10 @@

         public Pkg pkg3;
         public PkgVersion pkg3Version1;
+
+        public Pkg pkgAny;
+        public PkgVersion pkgAnyVersion1;
+
     }

 }
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java Sun Aug 31 10:18:17 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java Sun Aug 31 12:05:02 2014 UTC
@@ -128,7 +128,7 @@
new Predicate<GetAllPkgCategoriesResult.PkgCategory>() {
                             @Override
public boolean apply(GetAllPkgCategoriesResult.PkgCategory input) {
-                                return input.code.equals("EDUCATION");
+                                return input.code.equals("education");
                             }
                         }
                 );
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/PkgApiIT.java Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/PkgApiIT.java Sun Aug 31 12:05:02 2014 UTC
@@ -33,6 +33,7 @@
 import javax.annotation.Resource;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;

 public class PkgApiIT extends AbstractIntegrationTest {

@@ -59,13 +60,13 @@

             {
PkgPkgCategory pkgPkgCategory = context.newObject(PkgPkgCategory.class); - pkgPkgCategory.setPkgCategory(PkgCategory.getByCode(context, "GAMES").get()); + pkgPkgCategory.setPkgCategory(PkgCategory.getByCode(context, "games").get()); pkg.addToManyTarget(Pkg.PKG_PKG_CATEGORIES_PROPERTY, pkgPkgCategory, true);
             }

             {
PkgPkgCategory pkgPkgCategory = context.newObject(PkgPkgCategory.class); - pkgPkgCategory.setPkgCategory(PkgCategory.getByCode(context, "BUSINESS").get()); + pkgPkgCategory.setPkgCategory(PkgCategory.getByCode(context, "business").get()); pkg.addToManyTarget(Pkg.PKG_PKG_CATEGORIES_PROPERTY, pkgPkgCategory, true);
             }

@@ -74,7 +75,7 @@

UpdatePkgCategoriesRequest request = new UpdatePkgCategoriesRequest();
         request.pkgName = data.pkg1.getName();
- request.pkgCategoryCodes = ImmutableList.of("BUSINESS", "DEVELOPMENT"); + request.pkgCategoryCodes = ImmutableList.of("business", "development");

         // ------------------------------------
         pkgApi.updatePkgCategories(request);
@@ -87,16 +88,16 @@
             ObjectContext context = serverRuntime.getContext();
             Pkg pkg = Pkg.getByName(context, data.pkg1.getName()).get();

- Assertions.assertThat(ImmutableSet.of("BUSINESS", "DEVELOPMENT")).isEqualTo(
-                ImmutableSet.copyOf(Iterables.transform(
-                        pkg.getPkgPkgCategories(),
-                        new Function<PkgPkgCategory, String>() {
-                            @Override
-                            public String apply(PkgPkgCategory input) {
-                                return input.getPkgCategory().getCode();
+ Assertions.assertThat(ImmutableSet.of("business", "development")).isEqualTo(
+                    ImmutableSet.copyOf(Iterables.transform(
+                            pkg.getPkgPkgCategories(),
+                            new Function<PkgPkgCategory, String>() {
+                                @Override
+                                public String apply(PkgPkgCategory input) {
+ return input.getPkgCategory().getCode();
+                                }
                             }
-                        }
-                ))
+                    ))
             );
         }

@@ -118,7 +119,7 @@
         SearchPkgsResult result = pkgApi.searchPkgs(request);
         // ------------------------------------

-        Assertions.assertThat(result.total).isEqualTo(3);
+ Assertions.assertThat(result.total).isEqualTo(4); // note includes the "any" package
         Assertions.assertThat(result.items.size()).isEqualTo(2);
         Assertions.assertThat(result.items.get(0).name).isEqualTo("pkg1");
         Assertions.assertThat(result.items.get(1).name).isEqualTo("pkg2");
@@ -690,13 +691,27 @@
         request.versionType = PkgVersionType.LATEST;
         request.architectureCode = "x86";
         request.naturalLanguageCode = "en";
-        request.pkgNames = ImmutableList.of("pkg1","pkg2","pkg3");
+ request.pkgNames = ImmutableList.of("pkg1","pkg2","pkg3","pkg4","pkgany"); // pkg4 does not exist

         // ------------------------------------
         GetBulkPkgResult result = pkgApi.getBulkPkg(request);
         // ------------------------------------

-        Assertions.assertThat(result.pkgs.size()).isEqualTo(3);
+ Assertions.assertThat(result.pkgs.size()).isEqualTo(4); // includes the any package
+
+        // check they are all there.
+
+        Set<String> packageNames = ImmutableSet.copyOf(Lists.transform(
+                result.pkgs,
+                new Function<GetBulkPkgResult.Pkg, String>() {
+                    @Override
+                    public String apply(GetBulkPkgResult.Pkg input) {
+                        return input.name;
+                    }
+                }
+        ));
+
+ Assertions.assertThat(packageNames).containsOnly("pkg1","pkg2","pkg3","pkgany");

         // now check pkg1 because it has some in-depth data on it.

@@ -711,7 +726,7 @@
         Assertions.assertThat(pkg1.modifyTimestamp).isNotNull();

         Assertions.assertThat(pkg1.pkgCategoryCodes.size()).isEqualTo(1);
- Assertions.assertThat(pkg1.pkgCategoryCodes.get(0)).isEqualTo("GRAPHICS"); + Assertions.assertThat(pkg1.pkgCategoryCodes.get(0)).isEqualTo("graphics");

         Assertions.assertThat(pkg1.derivedRating).isNotNull();
Assertions.assertThat(pkg1.derivedRating).isGreaterThanOrEqualTo(0.0f);
@@ -728,11 +743,11 @@
         Assertions.assertThat(pkg1.pkgIcons.size()).isEqualTo(3);
         Assertions.assertThat(
Iterables.tryFind(pkg1.pkgIcons, new Predicate<org.haikuos.haikudepotserver.api1.model.pkg.PkgIcon>() {
-            @Override
- public boolean apply(org.haikuos.haikudepotserver.api1.model.pkg.PkgIcon input) { - return input.mediaTypeCode.equals(org.haikuos.haikudepotserver.dataobjects.MediaType.MEDIATYPE_HAIKUVECTORICONFILE);
-            }
-        }).isPresent()).isTrue();
+                    @Override
+ public boolean apply(org.haikuos.haikudepotserver.api1.model.pkg.PkgIcon input) { + return input.mediaTypeCode.equals(org.haikuos.haikudepotserver.dataobjects.MediaType.MEDIATYPE_HAIKUVECTORICONFILE);
+                    }
+                }).isPresent()).isTrue();

         Assertions.assertThat(pkg1.versions.size()).isEqualTo(1);
Assertions.assertThat(pkg1.versions.get(0).naturalLanguageCode).isEqualTo("en");

Other related posts:

  • » [haiku-depot-web] [haiku-depot-web-app] 4 new revisions pushed by haiku.li...@xxxxxxxxx on 2014-08-31 19:38 GMT - haiku-depot-web-app