diff --git a/LICENSE.TXT b/LICENSE.TXT new file mode 100644 index 00000000..5e1186fb --- /dev/null +++ b/LICENSE.TXT @@ -0,0 +1,3 @@ +These software source code resources are distributed under the +terms of the MIT License. +http://opensource.org/licenses/MIT diff --git a/README.TXT b/README.TXT new file mode 100644 index 00000000..a04effff --- /dev/null +++ b/README.TXT @@ -0,0 +1,76 @@ +Haiku Depot Server +~~~~~~~~~~~~~~~~~~ + +This collection of files represents the source code for the "Haiku Depot Server"; a web-services and HTML environment for working with Haiku packages. This is a maven java project that primarily builds an application server. + +--- +REQUIREMENTS, BUILD AND SETUP + +To build and use this software, you will require; + +* Java JDK 1.6 or better +* Apache Maven 3.0.3 or better +* A Postgres 9.1 or better database server +* An internet connection + +On a debian 7 host, the following packages can be installed; + + apt-get install default-jdk + apt-get install maven + apt-get install postgresql postgresql-client + +The project consists of a number of modules. The "haikudepotserver-webapp" is the application server module. This module requires a database in order to function. It is expecting to work with a Postgres database server. Create a blank postgres database and ensure that you are able to access the database over an internet socket authenticating as some user. You can leave the database devoid of schema objects for now because the application server will populate necessary schema objects on the first launch. + +The following file in the "haikudepotserver-webapp" is a template configuration file for the application server; + + src/main/resources/local-sample.properties + +Copy this to an adjacent file in the same directory called "local.properties". You will need to edit properties starting with "jdbc..." in order to let the application server know how to access your postgres database. + +The first build will take longer than 'normal' because it will need to download a number of dependencies from the internet in order to undertake the build. Some downloads are related to web-resources. These downloads are only indirectly managed by the maven build process. For this reason, your first step should be to complete a command-line build by issuing the following command in the same directory as this file; + + mvn package + +This step will ensure that the web-resources are populated. You should now be able to either continue to use the project in the command line environment or you can switch to use a java IDE. + +To start-up the application server for development purposes, issue the following command from the same top level of the project; the same directory as this file. + + mvn org.apache.tomcat.maven:tomcat7-maven-plugin:2.1:run + +This may take some time to start-up; especially the first time. Once it has started-up, it should be possible to connect to the application server using the following URL; + + http://localhost:8080/ + +There won't be any repositories or data loaded, and because of this, it is not possible to view any data. Now a repository can be added to obtain packages from. Open a SQL terminal and add a repository; + + INSERT INTO + haikudepot.repository ( + id, active, create_timestamp, modify_timestamp, + architecture_id, code, url) + VALUES ( + nextval('haikudepot.repository_seq'), true, now(), now(), + (SELECT id FROM haikudepot.architecture WHERE code='x86'), 'test', 'file:///tmp/repo.hpkr'); + +This artificial repository will obtain a file from the local file system's temporary directory. You could, for example, take the test HPKR file supplied in the test resources of the "haikudepotserver-packagefile" module and place this at /tmp/repo.hpkr. Now it should be possible to prompt the system to take-up the repository data by dispatching a URL of this form using a tool such as curl; + + curl "http://localhost:8080/importrepositorydata?code=test" + +You should now refresh your browser and it ought to be possible to view the packages that have been imported from the test file. + +--- +API + +The API for communicating with the server is described in the "haikudepotserver-api1" module. This contains DTO model objects describing the objects to be used in API calls as well as interfaces that describe the API calls that are available. The application server vends the API as JSON-RPC. More information about JSON-RPC can be found here; + + http://www.jsonrpc.org/ + +This API is intended to be used for the single-page web application as well as a desktop application. + +--- +HPKR HANDLING + +Haiku packages are described using HPK* files and these are described here; + + http://dev.haiku-os.org/wiki/PackageManagement + +The "haikudepotserver-packagefile" module contains handling for HPKR files. Given the requirements of the application server this handling is limited to read-only access. diff --git a/haikudepotserver-api1/pom.xml b/haikudepotserver-api1/pom.xml new file mode 100644 index 00000000..2bec96ed --- /dev/null +++ b/haikudepotserver-api1/pom.xml @@ -0,0 +1,25 @@ + + + + haikudepotserver-parent + org.haikuos + ../haikudepotserver-parent + 1.0.1-SNAPSHOT + + + 4.0.0 + org.haikuos + haikudepotserver-api1 + jar + + + + com.googlecode + jsonrpc4j + + + + diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/CaptchaApi.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/CaptchaApi.java new file mode 100644 index 00000000..cc06b917 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/CaptchaApi.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.googlecode.jsonrpc4j.JsonRpcService; +import org.haikuos.haikudepotserver.api1.model.captcha.GenerateCaptchaRequest; +import org.haikuos.haikudepotserver.api1.model.captcha.GenerateCaptchaResult; + +/** + *

This API is to do with captchas. A captcha is a small image that is shown to a user in order for the user to + * supply some textual response from the image in order to verify that the operator is likely to be human and not a + * computer. This helps to prevent machine-hacking of systems. This API is able to provide a captcha and other + * APIs require that a 'captcha response' is supplied as part of a request. In general a captcha is valid for a + * certain length of time.

+ */ + +@JsonRpcService("/api/v1/captcha") +public interface CaptchaApi { + + /** + *

This method will return a captcha that can be used in systems where a captcha response (generated by a + * human) is required to be supplied with an API request.

+ */ + + GenerateCaptchaResult generateCaptcha(GenerateCaptchaRequest generateCaptchaRequest); + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java new file mode 100644 index 00000000..f4c73430 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.googlecode.jsonrpc4j.JsonRpcService; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllArchitecturesRequest; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllArchitecturesResult; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllMessagesRequest; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllMessagesResult; + +@JsonRpcService("/api/v1/miscellaneous") +public interface MiscellaneousApi { + + /** + *

This method will return all of the localization messages that might be able to be displayed + * to the user from the result of validation problems and so on.

+ */ + + GetAllMessagesResult getAllMessages(GetAllMessagesRequest getAllMessagesRequest); + + /** + *

This method will return a list of all of the possible architectures in the system such as x86 or arm. + * Note that this will explicitly exclude the pseudo-architectures of "source" and "any".

+ */ + + GetAllArchitecturesResult getAllArchitectures(GetAllArchitecturesRequest getAllArchitecturesRequest); + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java new file mode 100644 index 00000000..f23b2731 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/PkgApi.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.googlecode.jsonrpc4j.JsonRpcService; +import org.haikuos.haikudepotserver.api1.model.pkg.GetPkgRequest; +import org.haikuos.haikudepotserver.api1.model.pkg.GetPkgResult; +import org.haikuos.haikudepotserver.api1.model.pkg.SearchPkgsRequest; +import org.haikuos.haikudepotserver.api1.model.pkg.SearchPkgsResult; +import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException; + +/** + *

This API is for access to packages and package versions.

+ */ + +@JsonRpcService("/api/v1/pkg") +public interface PkgApi { + + /** + *

This method can be invoked to get a list of all of the packages that match some search critera in the + * request.

+ */ + + SearchPkgsResult searchPkgs(SearchPkgsRequest request); + + /** + *

This method will return a package and the specified versions. It will throw an + * {@link org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException} if the package was not able to be located.

+ */ + + GetPkgResult getPkg(GetPkgRequest request) throws ObjectNotFoundException; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java new file mode 100644 index 00000000..9e3e24f8 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.googlecode.jsonrpc4j.JsonRpcService; +import org.haikuos.haikudepotserver.api1.model.user.*; +import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException; + +/** + *

This interface defines operations that can be undertaken around users.

+ */ + +@JsonRpcService("/api/v1/user") +public interface UserApi { + + /** + *

This method will create a user in the system. It is identified by a username + * and authenticated by a password. The password is supplied in the clear.

+ */ + + CreateUserResult createUser(CreateUserRequest createUserRequest); + + /** + *

This method will get the user identified by the nickname in the request object. + * If no user was able to be found an instance of {@link org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException} + * is thrown.

+ */ + + GetUserResult getUser(GetUserRequest getUserRequest) throws ObjectNotFoundException; + + /** + *

This method will allow a client to authenticate against the server. If this is + * successful then the client will know that it is OK to use the authentication + * principal and credentials for further API calls.

+ */ + + AuthenticateUserResult authenticateUser(AuthenticateUserRequest authenticateUserRequest); + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/captcha/GenerateCaptchaRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/captcha/GenerateCaptchaRequest.java new file mode 100644 index 00000000..2ad01969 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/captcha/GenerateCaptchaRequest.java @@ -0,0 +1,9 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.captcha; + +public class GenerateCaptchaRequest { +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/captcha/GenerateCaptchaResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/captcha/GenerateCaptchaResult.java new file mode 100644 index 00000000..818037af --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/captcha/GenerateCaptchaResult.java @@ -0,0 +1,23 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.captcha; + +public class GenerateCaptchaResult { + + /** + *

This token uniquely identifies the captcha.

+ */ + + public String token; + + /** + *

This is a base-64 encoded image of the captcha. It could, for example, be used with a data url to render + * the image in an "img" tag on a web page.

+ */ + + public String pngImageDataBase64; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllArchitecturesRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllArchitecturesRequest.java new file mode 100644 index 00000000..5d62daae --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllArchitecturesRequest.java @@ -0,0 +1,9 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.miscellaneous; + +public class GetAllArchitecturesRequest { +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllArchitecturesResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllArchitecturesResult.java new file mode 100644 index 00000000..39a2beeb --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllArchitecturesResult.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.miscellaneous; + +import java.util.List; + +public class GetAllArchitecturesResult { + + public List architectures; + + public static class Architecture { + public String code; + } + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllMessagesRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllMessagesRequest.java new file mode 100644 index 00000000..51729838 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllMessagesRequest.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.miscellaneous; + +public class GetAllMessagesRequest { + + // add locale here? + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllMessagesResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllMessagesResult.java new file mode 100644 index 00000000..a2c72dbe --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GetAllMessagesResult.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.miscellaneous; + +import java.util.Map; + +public class GetAllMessagesResult { + + /** + *

This is a key-value pair map of the localization messages.

+ */ + + public Map messages; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgRequest.java new file mode 100644 index 00000000..37b82d9e --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgRequest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.pkg; + +public class GetPkgRequest { + + /** + *

This type defines the versions that should be sent back in the result. If the client were + * only interested in the latest version for example, then it should use the "LATEST" value.

+ */ + + public enum VersionType { + LATEST + } + + /** + *

This is the name of the package that you wish to obtain.

+ */ + + public String name; + + /** + *

Only a version of the package for this architecture will be returned. Note that this also + * includes the pseudo-architectures "any" and "source".

+ */ + + public String architectureCode; + + public VersionType versionType; + + // TODO - natural language + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgResult.java new file mode 100644 index 00000000..33881555 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/GetPkgResult.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.pkg; + +import java.util.List; + +/** + *

This is the result model that comes back from the get packages API invocation.

+ */ + +public class GetPkgResult { + + public String name; + + public List versions; + + public static class Version { + + public String major; + public String minor; + public String micro; + public String preRelease; + public Integer revision; + + public String architectureCode; + public String summary; + public String description; + public String repositoryCode; + + public List licenses; + public List copyrights; + public List urls; + + } + + public static class Url { + + public String url; + public String urlTypeCode; + + } + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/SearchPkgsRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/SearchPkgsRequest.java new file mode 100644 index 00000000..79fb4b0a --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/SearchPkgsRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.pkg; + +/** + *

This is the model object that is used to define the request to search for packages in the system.

+ */ + +public class SearchPkgsRequest { + + public enum ExpressionType { + CONTAINS + } + + public String expression; + + public String architectureCode; + + public ExpressionType expressionType; + + public Integer offset; + + public Integer limit; + + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/SearchPkgsResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/SearchPkgsResult.java new file mode 100644 index 00000000..63897ee7 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/pkg/SearchPkgsResult.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.pkg; + +import java.util.List; + +public class SearchPkgsResult { + + public List pkgs; + + public Boolean hasMore; + + public static class Pkg { + public String name; + public Version version; + } + + public static class Version { + public String major; + public String minor; + public String micro; + public String preRelease; + public Integer revision; + } + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/AuthenticateUserRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/AuthenticateUserRequest.java new file mode 100644 index 00000000..84f73525 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/AuthenticateUserRequest.java @@ -0,0 +1,13 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class AuthenticateUserRequest { + + public String nickname; + public String passwordClear; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/AuthenticateUserResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/AuthenticateUserResult.java new file mode 100644 index 00000000..db6c41ec --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/AuthenticateUserResult.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class AuthenticateUserResult { + + public Boolean authenticated; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/CreateUserRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/CreateUserRequest.java new file mode 100644 index 00000000..105ff72b --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/CreateUserRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class CreateUserRequest { + + public String nickname; + public String passwordClear; + + /** + *

The captcha token is obtained from an earlier invocation to the + * {@link org.haikuos.haikudepotserver.api1.CaptchaApi} method to get + * a captcha. This identifies the captcha for which the captcha response should + * correlate.

+ */ + + public String captchaToken; + + /** + *

This is the human-supplied text string that matches the image that would have been + * provided with the captcha that is identified by the cpatchaToken.

+ */ + + public String captchaResponse; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/CreateUserResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/CreateUserResult.java new file mode 100644 index 00000000..aadc1713 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/CreateUserResult.java @@ -0,0 +1,9 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class CreateUserResult { +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/GetUserRequest.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/GetUserRequest.java new file mode 100644 index 00000000..b0078dcd --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/GetUserRequest.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class GetUserRequest { + + public String nickname; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/GetUserResult.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/GetUserResult.java new file mode 100644 index 00000000..d58a5bca --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/GetUserResult.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class GetUserResult { + + public String nickname; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/CaptchaBadResponseException.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/CaptchaBadResponseException.java new file mode 100644 index 00000000..b3515535 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/CaptchaBadResponseException.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.support; + +/** + *

This exception is thrown in the case where the user has mis-entered a captcha.

+ */ + +public class CaptchaBadResponseException extends RuntimeException { + + public CaptchaBadResponseException() { + super(); + } + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/Constants.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/Constants.java new file mode 100644 index 00000000..c1cb574a --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/Constants.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.support; + +public interface Constants { + + public final static int ERROR_CODE_VALIDATION = -32800; + public final static int ERROR_CODE_OBJECTNOTFOUND = -32801; + public final static int ERROR_CODE_CAPTCHABADRESPONSE = -32802; + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ObjectNotFoundException.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ObjectNotFoundException.java new file mode 100644 index 00000000..2db3f056 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ObjectNotFoundException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.support; + +/** + *

This exception is thrown when the system is not able to find an object in the system as part of an API + * invocation.

+ */ + +public class ObjectNotFoundException extends Exception { + + public String entityName; + public Object identifier; + + public ObjectNotFoundException(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()); + } +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ValidationException.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ValidationException.java new file mode 100644 index 00000000..0838af33 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ValidationException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.support; + +import java.util.Collections; +import java.util.List; + +/** + *

This exception is thrown in the situation where a validation exception is thrown outside of the Cayenne + * infrastructure.

+ */ + +public class ValidationException extends RuntimeException { + + protected List validationFailures; + + public ValidationException(ValidationFailure validationFailure) { + super(); + + if(null==validationFailure) { + throw new IllegalStateException(); + } + + this.validationFailures = Collections.singletonList(validationFailure); + } + + public ValidationException(List validationFailures) { + super(); + + if(null==validationFailures) { + throw new IllegalStateException(); + } + + this.validationFailures = validationFailures; + } + + public List getValidationFailures() { + return validationFailures; + } + + @Override + public String getMessage() { + return String.format("%d validation failures",validationFailures.size()); + } + +} diff --git a/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ValidationFailure.java b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ValidationFailure.java new file mode 100644 index 00000000..ab2eae74 --- /dev/null +++ b/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/support/ValidationFailure.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.support; + +/** + *

This object models a validation failure in the system. The property indicates what element of the object in + * question has failed validation checks and the message indicates what was wrong with that element.

+ */ + +public class ValidationFailure { + + private String property; + private String message; + + public ValidationFailure(String property, String message) { + super(); + + if(null==property || 0==property.length()) { + throw new IllegalStateException("the property is required for a validation failure"); + } + + if(null==message || 0==message.length()) { + throw new IllegalStateException("the message is required for a validation failure"); + } + + this.property = property; + this.message = message; + } + + public String getProperty() { + return property; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return String.format("%s; %s",getProperty(),getMessage()); + } + +} diff --git a/haikudepotserver-packagefile/pom.xml b/haikudepotserver-packagefile/pom.xml new file mode 100644 index 00000000..fa29e93c --- /dev/null +++ b/haikudepotserver-packagefile/pom.xml @@ -0,0 +1,53 @@ + + + + haikudepotserver-parent + org.haikuos + ../haikudepotserver-parent + 1.0.1-SNAPSHOT + + + 4.0.0 + org.haikuos + haikudepotserver-packagefile + jar + + + + + args4j + args4j + + + + com.google.guava + guava + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + junit + junit + test + + + + org.easytesting + fest-assert + test + + + + + diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/AttributeContext.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/AttributeContext.java new file mode 100644 index 00000000..1dff0367 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/AttributeContext.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import org.haikuos.pkg.heap.HeapReader; + +/** + *

This object carries around pointers to other data structures and model objects that are required to + * support the processing of attributes.

+ */ + +public class AttributeContext { + + private StringTable stringTable; + + private HeapReader heapReader; + + public HeapReader getHeapReader() { + return heapReader; + } + + public void setHeapReader(HeapReader heapReader) { + this.heapReader = heapReader; + } + + public StringTable getStringTable() { + return stringTable; + } + + public void setStringTable(StringTable stringTable) { + this.stringTable = stringTable; + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/AttributeIterator.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/AttributeIterator.java new file mode 100644 index 00000000..885b12db --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/AttributeIterator.java @@ -0,0 +1,315 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import org.haikuos.pkg.heap.HeapCoordinates; +import org.haikuos.pkg.model.*; + +import java.math.BigInteger; +import java.util.Iterator; + +/** + *

This object is able to provide an iterator through all of the attributes at a given offset in the chunks. The + * chunk data is supplied through an instance of {@link AttributeContext}. It will work through all of the + * attributes serially and will also process all of the child-attributes as well. The iteration process means that + * less in-memory data is required to process a relatively long list of attributes.

+ * + *

Use the method {@link #hasNext()} to find out if there is another attribute to read and {@link #next()} in + * order to obtain the next attribute.

+ * + *

Note that this does not actually implement {@link Iterator} because it needs to throw Hpk exceptions + * which would mean that it were not compliant with the @{link Iterator} interface.

+ */ + +public class AttributeIterator { + + private final static int ATTRIBUTE_TYPE_INVALID = 0; + private final static int ATTRIBUTE_TYPE_INT = 1; + private final static int ATTRIBUTE_TYPE_UINT = 2; + private final static int ATTRIBUTE_TYPE_STRING = 3; + private final static int ATTRIBUTE_TYPE_RAW = 4; + + private final static int ATTRIBUTE_ENCODING_INT_8_BIT = 0; + private final static int ATTRIBUTE_ENCODING_INT_16_BIT = 1; + private final static int ATTRIBUTE_ENCODING_INT_32_BIT = 2; + private final static int ATTRIBUTE_ENCODING_INT_64_BIT = 3; + + private final static int ATTRIBUTE_ENCODING_STRING_INLINE = 0; + private final static int ATTRIBUTE_ENCODING_STRING_TABLE = 1; + + private final static int ATTRIBUTE_ENCODING_RAW_INLINE = 0; + private final static int ATTRIBUTE_ENCODING_RAW_HEAP = 1; + + private long offset; + + private AttributeContext context; + + private BigInteger nextTag = null; + + public AttributeIterator(AttributeContext context, long offset) { + super(); + + Preconditions.checkNotNull(context); + Preconditions.checkState(offset >= 0 && offset < Integer.MAX_VALUE); + + this.offset = offset; + this.context = context; + } + + public AttributeContext getContext() { + return context; + } + + public long getOffset() { + return offset; + } + + private int deriveAttributeTagType(BigInteger tag) { + return tag.subtract(BigInteger.valueOf(1l)).shiftRight(7).and(BigInteger.valueOf(0x7l)).intValue(); + } + + private int deriveAttributeTagId(BigInteger tag) { + return tag.subtract(BigInteger.valueOf(1l)).and(BigInteger.valueOf(0x7fl)).intValue(); + } + + private int deriveAttributeTagEncoding(BigInteger tag) { + return tag.subtract(BigInteger.valueOf(1l)).shiftRight(11).and(BigInteger.valueOf(3l)).intValue(); + } + + private boolean deriveAttributeTagHasChildAttributes(BigInteger tag) { + return 0!=tag.subtract(BigInteger.valueOf(1l)).shiftRight(10).and(BigInteger.valueOf(1l)).intValue(); + } + + private BigInteger getNextTag() { + if(null==nextTag) { + nextTag = readUnsignedLeb128(); + } + + return nextTag; + } + + private BigInteger readUnsignedLeb128() { + BigInteger result = BigInteger.valueOf(0l); + int shift = 0; + + while(true) { + int b = context.getHeapReader().readHeap(offset); + offset++; + + result = result.or(BigInteger.valueOf((long) (b & 0x7f)).shiftLeft(shift)); + + if(0 == (b & 0x80)) { + return result; + } + + shift+=7; + } + } + + private void ensureValidEncodingForInt(int encoding) { + switch(encoding) { + case ATTRIBUTE_ENCODING_INT_8_BIT: + case ATTRIBUTE_ENCODING_INT_16_BIT: + case ATTRIBUTE_ENCODING_INT_32_BIT: + case ATTRIBUTE_ENCODING_INT_64_BIT: + break; + + default: + throw new IllegalStateException("unknown encoding on a signed integer"); + } + } + + /** + *

This method allows the caller to discover if there is another attribute to get off the iterator.

+ */ + + public boolean hasNext() { + return 0!=getNextTag().signum(); + } + + /** + *

This method will return the next {@link org.haikuos.pkg.model.Attribute}. If there is not another value to return then + * this method will return null. It will throw an instance of @{link HpkException} in any situation in which + * it is not able to parse the data or chunks such that it is not able to read the next attribute.

+ */ + + public Attribute next() throws HpkException { + + Attribute result = null; + + // first, the LEB128 has to be read in which is the 'tag' defining what sort of attribute this is that + // we are dealing with. + + BigInteger tag = getNextTag(); + + // if we encounter 0 tag then we know that we have finished the list. + + if(0!=tag.signum()) { + + int encoding = deriveAttributeTagEncoding(tag); + int id = deriveAttributeTagId(tag); + + if(id <= 0 || id >= AttributeId.values().length) { + throw new HpkException("illegal id; "+Integer.toString(id)); + } + AttributeId attributeId = AttributeId.values()[id]; + + switch(deriveAttributeTagType(tag)) { + + case ATTRIBUTE_TYPE_INVALID: + throw new HpkException("an invalid attribute tag type has been encountered"); + + case ATTRIBUTE_TYPE_INT: + { + ensureValidEncodingForInt(encoding); + byte[] buffer = new byte[encoding+1]; + context.getHeapReader().readHeap(buffer,0,new HeapCoordinates(offset,encoding+1)); + offset+=encoding+1; + result = new IntAttribute(attributeId, new BigInteger(buffer)); + } + break; + + case ATTRIBUTE_TYPE_UINT: + { + ensureValidEncodingForInt(encoding); + byte[] buffer = new byte[encoding+1]; + context.getHeapReader().readHeap(buffer,0,new HeapCoordinates(offset,encoding+1)); + offset+=encoding+1; + result = new IntAttribute(attributeId, new BigInteger(1,buffer)); + } + break; + + case ATTRIBUTE_TYPE_STRING: + { + switch(encoding) { + + case ATTRIBUTE_ENCODING_STRING_INLINE: + { + ByteArrayDataOutput assembly = ByteStreams.newDataOutput(); + + while(null==result) { + int b = context.getHeapReader().readHeap(offset); + offset++; + + if(0!=b) { + assembly.write(b); + } + else { + result = new StringInlineAttribute( + attributeId, + new String( + assembly.toByteArray(), + Charsets.UTF_8)); + } + } + } + break; + + case ATTRIBUTE_ENCODING_STRING_TABLE: + { + BigInteger index = readUnsignedLeb128(); + + if(index.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new IllegalStateException("the string table index is preposterously large"); + } + + result = new StringTableRefAttribute(attributeId,index.intValue()); + } + break; + + default: + throw new HpkException("unknown string encoding; "+encoding); + } + } + break; + + case ATTRIBUTE_TYPE_RAW: + { + switch(encoding) { + case ATTRIBUTE_ENCODING_RAW_INLINE: + { + BigInteger length = readUnsignedLeb128(); + + if(length.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new HpkException("the length of the inline data is too large"); + } + + byte[] buffer = new byte[length.intValue()]; + context.getHeapReader().readHeap(buffer,0,new HeapCoordinates(offset,length.intValue())); + offset += length.intValue(); + + result = new RawInlineAttribute(attributeId, buffer); + } + break; + + case ATTRIBUTE_ENCODING_RAW_HEAP: + { + BigInteger rawLength = readUnsignedLeb128(); + BigInteger rawOffset = readUnsignedLeb128(); + + if(rawLength.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new HpkException("the length of the heap data is too large"); + } + + if(rawOffset.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new HpkException("the offset of the heap data is too large"); + } + + result = new RawHeapAttribute( + attributeId, + new HeapCoordinates( + rawOffset.longValue(), + rawLength.longValue())); + } + + default: + throw new HpkException("unknown raw encoding; "+encoding); + } + } + break; + + default: + throw new HpkException("unable to read the tag type; "+deriveAttributeTagType(tag)); + + } + + // each attribute id has a type associated with it; now check that the attribute matches + // its intended type. + + if(result.getAttributeId().getAttributeType() != result.getAttributeType()) { + throw new HpkException(String.format( + "mismatch in attribute type for id %s; expecting %s, but got %s", + result.getAttributeId().getName(), + result.getAttributeId().getAttributeType(), + result.getAttributeType())); + } + + // possibly there are child attributes after this attribute; if this is the + // case then open-up a new iterator to work across those and load them in. + + if(deriveAttributeTagHasChildAttributes(tag)) { + + AttributeIterator childAttributeIterator = new AttributeIterator(context, offset); + + while(childAttributeIterator.hasNext()) { + result.addChildAttribute(childAttributeIterator.next()); + } + + offset = childAttributeIterator.getOffset(); + + } + + nextTag = null; + } + + return result; + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/FileHelper.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/FileHelper.java new file mode 100644 index 00000000..6ce1278d --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/FileHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.math.BigInteger; + +/** + *

This helps out with typical common reads that might be performed as part of + * parsing various values in the HPKR file.

+ */ + +public class FileHelper { + + private final static BigInteger MAX_BIGINTEGER_FILE = new BigInteger(Long.toString(Long.MAX_VALUE)); + + private byte[] buffer8 = new byte[8]; + + public int readUnsignedShortToInt(RandomAccessFile randomAccessFile) throws IOException, HpkException { + + if(2!=randomAccessFile.read(buffer8,0,2)) { + throw new HpkException("not enough bytes read for an unsigned short"); + } + + int i0 = buffer8[0]&0xff; + int i1 = buffer8[1]&0xff; + + return i0 << 8 | i1; + } + + public long readUnsignedIntToLong(RandomAccessFile randomAccessFile) throws IOException, HpkException { + + if(4!=randomAccessFile.read(buffer8,0,4)) { + throw new HpkException("not enough bytes read for an unsigned int"); + } + + long l0 = buffer8[0]&0xff; + long l1 = buffer8[1]&0xff; + long l2 = buffer8[2]&0xff; + long l3 = buffer8[3]&0xff; + + return l0 << 24 | l1 << 16 | l2 << 8 | l3; + } + + public BigInteger readUnsignedLong(RandomAccessFile randomAccessFile) throws IOException, HpkException { + + if(8!=randomAccessFile.read(buffer8)) { + throw new HpkException("not enough bytes read for an unsigned long"); + } + + return new BigInteger(1, buffer8); + } + + public long readUnsignedLongToLong(RandomAccessFile randomAccessFile) throws IOException, HpkException { + + BigInteger result = readUnsignedLong(randomAccessFile); + + if(result.compareTo(MAX_BIGINTEGER_FILE) > 0) { + throw new HpkException("the hpkr file contains an unsigned long which is larger than can be represented in a java long"); + } + + return result.longValue(); + } + + public char[] readMagic(RandomAccessFile randomAccessFile) throws IOException, HpkException { + + if(4!=randomAccessFile.read(buffer8,0,4)) { + throw new HpkException("not enough bytes read for a 4-byte magic"); + } + + return new char[] { + (char) buffer8[0], + (char) buffer8[1], + (char) buffer8[2], + (char) buffer8[3] + }; + } + + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkException.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkException.java new file mode 100644 index 00000000..9e380b92 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +/** + *

This type of exception is used through the Hpk file processing system to indicate that something has gone wrong + * with processing the Hpk data in some way.

+ */ + +public class HpkException extends Exception { + + public HpkException(String message) { + super(message); + } + + public HpkException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkStringTable.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkStringTable.java new file mode 100644 index 00000000..847c0b04 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkStringTable.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import org.haikuos.pkg.heap.HeapCoordinates; +import org.haikuos.pkg.heap.HpkHeapReader; + +/** + *

The HPK* file format may contain a table of commonly used strings in a table. This object will represent + * those strings and will lazily load them from the heap as necessary.

+ */ + +public class HpkStringTable implements StringTable { + + private HpkHeapReader heapReader; + + private long expectedCount; + + private long heapLength; + + private long heapOffset; + + private String[] values = null; + + public HpkStringTable( + HpkHeapReader heapReader, + long heapOffset, + long heapLength, + long expectedCount) { + + super(); + + Preconditions.checkNotNull(heapReader); + Preconditions.checkState(heapOffset >= 0 && heapOffset < Integer.MAX_VALUE); + Preconditions.checkState(heapLength >= 0 && heapLength < Integer.MAX_VALUE); + Preconditions.checkState(expectedCount >= 0 && expectedCount < Integer.MAX_VALUE); + + this.heapReader = heapReader; + this.expectedCount = expectedCount; + this.heapOffset = heapOffset; + this.heapLength = heapLength; + + } + + // TODO; could avoid the big read into a buffer by reading the heap byte by byte. + private String[] readStrings() throws HpkException { + String[] result = new String[(int) expectedCount]; + byte[] stringsDataBuffer = new byte[(int) heapLength]; + + heapReader.readHeap( + stringsDataBuffer, + 0, + new HeapCoordinates( + heapOffset, + heapLength)); + + // now work through the data and load them into the strings. + + int stringIndex = 0; + int offset = 0; + + while(offset < stringsDataBuffer.length) { + + if(0==stringsDataBuffer[offset]) { + if(stringIndex != result.length) { + throw new HpkException(String.format("expected to read %d package strings from the strings table, but actually found %d",expectedCount,stringIndex)); + } + + return result; + } + + int start = offset; + + while(0!=stringsDataBuffer[offset] && offset < stringsDataBuffer.length) { + offset++; + } + + if(offset < stringsDataBuffer.length) { + result[stringIndex] = new String(stringsDataBuffer,start,offset-start, Charsets.UTF_8); + stringIndex++; + offset++; + } + + } + + throw new HpkException("expected to find the null-terminator for the list of strings, but was not able to find one."); + } + + private String[] getStrings() throws HpkException { + if(null==values) { + if(0==heapLength) { + values = new String[] {}; + } + else { + values = readStrings(); + } + } + + return values; + } + + @Override + public String getString(int index) throws HpkException { + return getStrings()[index]; + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkrFileExtractor.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkrFileExtractor.java new file mode 100644 index 00000000..e0d08465 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkrFileExtractor.java @@ -0,0 +1,152 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.heap.HeapCompression; +import org.haikuos.pkg.heap.HpkHeapReader; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; + +/** + *

This object represents an object that can extract an Hpkr (Haiku Pkg Repository) file. If you are wanting to + * read HPKR files then you should instantiate an instance of this class and then make method calls to it in order to + * read values such as the attributes of the HPKR file.

+ */ + +public class HpkrFileExtractor implements Closeable { + + private File file; + + private HpkrHeader header; + + private HpkHeapReader heapReader; + + private HpkStringTable attributesStringTable; + + public HpkrFileExtractor(File file) throws IOException, HpkException { + + super(); + Preconditions.checkNotNull(file); + Preconditions.checkState(file.isFile() && file.exists()); + + this.file = file; + this.header = readHeader(); + + try { + heapReader = new HpkHeapReader( + file, + header.getHeapCompression(), + header.getHeaderSize(), + header.getHeapChunkSize(), // uncompressed size + header.getHeapSizeCompressed(), // including the compressed chunk lengths. + header.getHeapSizeUncompressed() // excludes the compressed chunk lengths. + ); + + attributesStringTable = new HpkStringTable( + heapReader, + header.getInfoLength(), + header.getPackagesStringsLength(), + header.getPackagesStringsCount()); + + } + catch(Exception e) { + close(); + throw new HpkException("unable to setup the hpkr file extractor",e); + } + catch(Throwable th) { + close(); + throw new RuntimeException("unable to setup the hpkr file extractor",th); + } + } + + @Override + public void close() { + if(null!=heapReader) { + heapReader.close(); + } + } + + public AttributeContext getAttributeContext() { + AttributeContext context = new AttributeContext(); + context.setHeapReader(heapReader); + context.setStringTable(attributesStringTable); + return context; + } + + public AttributeIterator getPackageAttributesIterator() { + long offset = header.getInfoLength() + header.getPackagesStringsLength(); + return new AttributeIterator(getAttributeContext(),offset); + } + + private HpkrHeader readHeader() throws IOException, HpkException { + Preconditions.checkNotNull(file); + + RandomAccessFile randomAccessFile = null; + FileHelper fileHelper = new FileHelper(); + + try { + randomAccessFile = new RandomAccessFile(file, "r"); + + if(!Arrays.equals(new char[] { 'h','p','k','r' }, fileHelper.readMagic(randomAccessFile))) { + throw new HpkException("magic incorrect at the start of the hpkr file"); + } + + HpkrHeader result = new HpkrHeader(); + + result.setHeaderSize(fileHelper.readUnsignedShortToInt(randomAccessFile)); + result.setVersion(fileHelper.readUnsignedShortToInt(randomAccessFile)); + result.setTotalSize(fileHelper.readUnsignedLongToLong(randomAccessFile)); + result.setMinorVersion(fileHelper.readUnsignedShortToInt(randomAccessFile)); + + int compression = fileHelper.readUnsignedShortToInt(randomAccessFile); + + // heap information + switch(compression) { + case 0: + result.setHeapCompression(HeapCompression.NONE); + break; + + case 1: + result.setHeapCompression(HeapCompression.ZLIB); + break; + + default: + throw new HpkException("unknown compression setting in header; "+compression); + } + + result.setHeapChunkSize(fileHelper.readUnsignedIntToLong(randomAccessFile)); + result.setHeapSizeCompressed(fileHelper.readUnsignedLongToLong(randomAccessFile)); + result.setHeapSizeUncompressed(fileHelper.readUnsignedLongToLong(randomAccessFile)); + + // repository info + result.setInfoLength(fileHelper.readUnsignedIntToLong(randomAccessFile)); + randomAccessFile.skipBytes(4); // reserved + + // package attributes section + result.setPackagesLength(fileHelper.readUnsignedLongToLong(randomAccessFile)); + result.setPackagesStringsLength(fileHelper.readUnsignedLongToLong(randomAccessFile)); + result.setPackagesStringsCount(fileHelper.readUnsignedLongToLong(randomAccessFile)); + + return result; + } + finally { + if(null!=randomAccessFile) { + try { + randomAccessFile.close(); + } + catch(IOException ioe) { + // ignore + } + } + } + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkrHeader.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkrHeader.java new file mode 100644 index 00000000..3fa53bf4 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/HpkrHeader.java @@ -0,0 +1,128 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import org.haikuos.pkg.heap.HeapCompression; + +public class HpkrHeader { + + private long headerSize; + private int version; + private long totalSize; + private int minorVersion; + + // heap + private HeapCompression heapCompression; + private long heapChunkSize; + private long heapSizeCompressed; + private long heapSizeUncompressed; + + // repository info section + private long infoLength; + + // package attributes section + private long packagesLength; + private long packagesStringsLength; + private long packagesStringsCount; + + public long getHeaderSize() { + return headerSize; + } + + public void setHeaderSize(long headerSize) { + this.headerSize = headerSize; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public long getTotalSize() { + return totalSize; + } + + public void setTotalSize(long totalSize) { + this.totalSize = totalSize; + } + + public int getMinorVersion() { + return minorVersion; + } + + public void setMinorVersion(int minorVersion) { + this.minorVersion = minorVersion; + } + + public HeapCompression getHeapCompression() { + return heapCompression; + } + + public void setHeapCompression(HeapCompression heapCompression) { + this.heapCompression = heapCompression; + } + + public long getHeapChunkSize() { + return heapChunkSize; + } + + public void setHeapChunkSize(long heapChunkSize) { + this.heapChunkSize = heapChunkSize; + } + + public long getHeapSizeCompressed() { + return heapSizeCompressed; + } + + public void setHeapSizeCompressed(long heapSizeCompressed) { + this.heapSizeCompressed = heapSizeCompressed; + } + + public long getHeapSizeUncompressed() { + return heapSizeUncompressed; + } + + public void setHeapSizeUncompressed(long heapSizeUncompressed) { + this.heapSizeUncompressed = heapSizeUncompressed; + } + + public long getInfoLength() { + return infoLength; + } + + public void setInfoLength(long infoLength) { + this.infoLength = infoLength; + } + + public long getPackagesLength() { + return packagesLength; + } + + public void setPackagesLength(long packagesLength) { + this.packagesLength = packagesLength; + } + + public long getPackagesStringsLength() { + return packagesStringsLength; + } + + public void setPackagesStringsLength(long packagesStringsLength) { + this.packagesStringsLength = packagesStringsLength; + } + + public long getPackagesStringsCount() { + return packagesStringsCount; + } + + public void setPackagesStringsCount(long packagesStringsCount) { + this.packagesStringsCount = packagesStringsCount; + } + +} + diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgException.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgException.java new file mode 100644 index 00000000..aafa5071 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgException.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +public class PkgException extends Exception { + + public PkgException(String message) { + super(message); + } + + public PkgException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgFactory.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgFactory.java new file mode 100644 index 00000000..6c8be8ae --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgFactory.java @@ -0,0 +1,158 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import org.haikuos.pkg.model.*; + +import java.math.BigInteger; + +/** + *

This object is algorithm that is able to convert a top level package attribute into a modelled package object + * that can more easily represent the package; essentially converting the low-level attributes into a higher-level + * package model object.

+ */ + +public class PkgFactory { + + private String getOptionalStringAttributeValue( + AttributeContext attributeContext, + Attribute attribute, + AttributeId attributeId) throws PkgException, HpkException { + + Preconditions.checkNotNull(attribute); + Preconditions.checkNotNull(attributeContext); + + Optional nameAttributeOptional = attribute.getChildAttribute(attributeId); + + if(!nameAttributeOptional.isPresent()) { + return null; + } + + return (String) nameAttributeOptional.get().getValue(attributeContext); + } + + private String getRequiredStringAttributeValue( + AttributeContext attributeContext, + Attribute attribute, + AttributeId attributeId) throws PkgException, HpkException { + + Preconditions.checkNotNull(attribute); + Preconditions.checkNotNull(attributeContext); + + Optional nameAttributeOptional = attribute.getChildAttribute(attributeId); + + if(!nameAttributeOptional.isPresent()) { + throw new PkgException(String.format("the %s attribute must be present",attributeId.getName())); + } + + return (String) nameAttributeOptional.get().getValue(attributeContext); + } + + private PkgVersion createVersion( + AttributeContext attributeContext, + Attribute attribute) throws PkgException, HpkException { + + Preconditions.checkNotNull(attribute); + Preconditions.checkNotNull(attributeContext); + Preconditions.checkState(AttributeId.PACKAGE_VERSION_MAJOR == attribute.getAttributeId()); + + Optional revisionAttribute = attribute.getChildAttribute(AttributeId.PACKAGE_VERSION_REVISION); + Integer revision = null; + + if(revisionAttribute.isPresent()) { + revision = ((BigInteger) ((IntAttribute) revisionAttribute.get()).getValue(attributeContext)).intValue(); + } + + return new PkgVersion( + (String) attribute.getValue(attributeContext), + getOptionalStringAttributeValue(attributeContext, attribute, AttributeId.PACKAGE_VERSION_MINOR), + getOptionalStringAttributeValue(attributeContext, attribute, AttributeId.PACKAGE_VERSION_MICRO), + getOptionalStringAttributeValue(attributeContext,attribute, AttributeId.PACKAGE_VERSION_PRE_RELEASE), + revision); + + } + + private PkgArchitecture createArchitecture( + AttributeContext attributeContext, + Attribute attribute) throws PkgException, HpkException { + + Preconditions.checkNotNull(attribute); + Preconditions.checkNotNull(attributeContext); + Preconditions.checkState(AttributeId.PACKAGE_ARCHITECTURE == attribute.getAttributeId()); + + int value = ((BigInteger) attribute.getValue(attributeContext)).intValue(); + return PkgArchitecture.values()[value]; + } + + public Pkg createPackage( + AttributeContext attributeContext, + Attribute attribute) throws PkgException { + + Preconditions.checkNotNull(attribute); + Preconditions.checkNotNull(attributeContext); + Preconditions.checkState(attribute.getAttributeId() == AttributeId.PACKAGE); + + Pkg result = new Pkg(); + + try { + + result.setName(getRequiredStringAttributeValue(attributeContext, attribute, AttributeId.PACKAGE_NAME)); + result.setVendor(getRequiredStringAttributeValue(attributeContext, attribute, AttributeId.PACKAGE_VENDOR)); + result.setSummary(getOptionalStringAttributeValue(attributeContext, attribute, AttributeId.PACKAGE_SUMMARY)); + result.setDescription(getOptionalStringAttributeValue(attributeContext, attribute, AttributeId.PACKAGE_DESCRIPTION)); + + result.setHomePageUrl(new PkgUrl( + getOptionalStringAttributeValue(attributeContext, attribute, AttributeId.PACKAGE_URL), + PkgUrlType.HOMEPAGE)); + + // get the architecture. + + Optional architectureAttributeOptional = attribute.getChildAttribute(AttributeId.PACKAGE_ARCHITECTURE); + + if(!architectureAttributeOptional.isPresent()) { + throw new PkgException(String.format("the attribute %s is required", AttributeId.PACKAGE_ARCHITECTURE)); + } + + result.setArchitecture(createArchitecture(attributeContext,architectureAttributeOptional.get())); + + // get the version. + + Optional majorVersionAttributeOptional = attribute.getChildAttribute(AttributeId.PACKAGE_VERSION_MAJOR); + + if(!majorVersionAttributeOptional.isPresent()) { + throw new PkgException(String.format("the attribute %s is required", AttributeId.PACKAGE_VERSION_MAJOR)); + } + + result.setVersion(createVersion(attributeContext, majorVersionAttributeOptional.get())); + + // get the copyrights. + + for(Attribute copyrightAttribute : attribute.getChildAttributes(AttributeId.PACKAGE_COPYRIGHT)) { + if(copyrightAttribute.getAttributeType() == AttributeType.STRING) { // illegal not to be, but be lenient + result.addCopyright(copyrightAttribute.getValue(attributeContext).toString()); + } + } + + // get the licenses. + + for(Attribute licenseAttribute : attribute.getChildAttributes(AttributeId.PACKAGE_LICENSE)) { + if(licenseAttribute.getAttributeType() == AttributeType.STRING) { // illegal not to be, but be lenient + result.addLicense(licenseAttribute.getValue(attributeContext).toString()); + } + } + + + } + catch(HpkException he) { + throw new PkgException("unable to create a package owing to a problem with the hpk packaging",he); + } + + return result; + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgIterator.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgIterator.java new file mode 100644 index 00000000..c185d391 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/PkgIterator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.model.Attribute; +import org.haikuos.pkg.model.Pkg; + +/** + *

This object will wrap an attribute iterator to be able to generate a series of {@link Pkg} objects that + * model a package in the HaikuOS package management system.

+ */ + +public class PkgIterator { + + private AttributeIterator attributeIterator; + private PkgFactory pkgFactory; + + public PkgIterator(AttributeIterator attributeIterator) { + this(attributeIterator, new PkgFactory()); + } + + public PkgIterator(AttributeIterator attributeIterator, PkgFactory pkgFactory) { + super(); + Preconditions.checkNotNull(attributeIterator); + this.attributeIterator = attributeIterator; + this.pkgFactory = pkgFactory; + } + + /** + *

This method will return true if there are more packages to be obtained from the attributes iterator.

+ * @return + */ + + public boolean hasNext() { + return attributeIterator.hasNext(); + } + + /** + *

This method will return the next package from the attribute iterator supplied.

+ * @return The return value is the next package from the list of attributes. + * @throws PkgException when there is a problem obtaining the next package from the attributes. + * @throws HpkException when there is a problem obtaining the next attributes. + */ + + public Pkg next() throws PkgException, HpkException { + Attribute attribute = attributeIterator.next(); + + if(null!=attribute) { + return pkgFactory.createPackage(attributeIterator.getContext(), attribute); + } + + return null; + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/StringTable.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/StringTable.java new file mode 100644 index 00000000..d878fc6b --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/StringTable.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +/** + *

The attribute-reading elements of the system need to be able to access a string table. This is interface of + * an object which is able to provide those strings.

+ */ + +public interface StringTable { + + /** + *

Given the index supplied, this method should return the corresponding string. It will throw an instance + * of {@link HpkException} if there is any problems associated with achieving this.

+ */ + + public String getString(int index) throws HpkException; + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapCompression.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapCompression.java new file mode 100644 index 00000000..4cd727f1 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapCompression.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.heap; + +public enum HeapCompression { + NONE, + ZLIB +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapCoordinates.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapCoordinates.java new file mode 100644 index 00000000..4a1a9921 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapCoordinates.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.heap; + +import com.google.common.base.Preconditions; + +/** + *

This object provides an offset and length into the heap and this provides a coordinate for a chunk of + * data in the heap. Note that the coordinates refer to the uncompressed data across all of the chunks of the heap. + *

+ */ + +public class HeapCoordinates { + + private long offset; + private long length; + + public HeapCoordinates(long offset, long length) { + super(); + + Preconditions.checkState(offset >= 0 && offset < Integer.MAX_VALUE); + Preconditions.checkState(length >= 0 && length < Integer.MAX_VALUE); + + this.offset = offset; + this.length = length; + } + + public long getOffset() { + return offset; + } + + public long getLength() { + return length; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HeapCoordinates that = (HeapCoordinates) o; + + if (length != that.length) return false; + if (offset != that.offset) return false; + + return true; + } + + @Override + public int hashCode() { + int result = (int) (offset ^ (offset >>> 32)); + result = 31 * result + (int) (length ^ (length >>> 32)); + return result; + } + + @Override + + public String toString() { + return String.format("{%d,%d}",offset,length); + } + + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapReader.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapReader.java new file mode 100644 index 00000000..721c0f86 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HeapReader.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.heap; + +/** + *

This is an interface for classes that are able to provide data from a block of memory referred to as "the heap". + * Concrete sub-classes are able to provide specific implementations that can read from different on-disk files to + * provide access to a heap. + *

+ */ + +public interface HeapReader { + + /** + *

This method reads from the heap (possibly across chunks) the data described in the coordinates attribute. It + * writes those bytes into the supplied buffer at the offset supplied.

+ */ + + public void readHeap(byte[] buffer, int bufferOffset, HeapCoordinates coordinates); + + /** + *

This method reads a single byte of the heap at the given offset.

+ */ + + public int readHeap(long offset); + +} \ No newline at end of file diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HpkHeapReader.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HpkHeapReader.java new file mode 100644 index 00000000..db96b150 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/heap/HpkHeapReader.java @@ -0,0 +1,347 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.heap; + +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import org.haikuos.pkg.FileHelper; +import org.haikuos.pkg.HpkException; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + *

An instance of this class is able to read the heap's chunks that are in HPK format. Note + * that this class will also take responsibility for caching the chunks so that a subsequent + * read from the same chunk will not require a re-fault from disk.

+ */ + +public class HpkHeapReader implements Closeable, HeapReader { + + private File file; + + private HeapCompression compression; + + private long heapOffset; + + private long chunkSize; + + private long compressedSize; // including the shorts for the chunks' compressed sizes + + private long uncompressedSize; // excluding the shorts for the chunks' compressed sizes + + private LoadingCache heapChunkUncompressedCache; + + private int[] heapChunkCompressedLengths = null; + + private RandomAccessFile randomAccessFile; + + private FileHelper fileHelper = new FileHelper(); + + public HpkHeapReader( + final File file, + final HeapCompression compression, + final long heapOffset, + final long chunkSize, + final long compressedSize, + final long uncompressedSize) throws HpkException { + + super(); + + Preconditions.checkNotNull(file); + Preconditions.checkNotNull(compression); + Preconditions.checkState(heapOffset > 0 &&heapOffset < Integer.MAX_VALUE); + Preconditions.checkState(chunkSize > 0 && chunkSize < Integer.MAX_VALUE); + Preconditions.checkState(compressedSize >= 0 && compressedSize < Integer.MAX_VALUE); + Preconditions.checkState(uncompressedSize >= 0 && compressedSize < Integer.MAX_VALUE); + + this.file = file; + this.compression = compression; + this.heapOffset = heapOffset; + this.chunkSize = chunkSize; + this.compressedSize = compressedSize; + this.uncompressedSize = uncompressedSize; + + try { + randomAccessFile = new RandomAccessFile(file,"r"); + + heapChunkCompressedLengths = new int[getHeapChunkCount()]; + populateChunkCompressedLengths(heapChunkCompressedLengths); + + heapChunkUncompressedCache = CacheBuilder + .newBuilder() + .maximumSize(3) + .build(new CacheLoader() { + @Override + public byte[] load(Integer key) throws Exception { + Preconditions.checkNotNull(key); + + // TODO: best to avoid continuously allocating new byte buffers + byte[] result = new byte[getHeapChunkUncompressedLength(key)]; + readHeapChunk(key,result); + return result; + } + }); + } + catch(Exception e) { + close(); + throw new HpkException("unable to configure the hpk heap reader",e); + } + catch(Throwable th) { + close(); + throw new RuntimeException("unable to configure the hkp heap reader",th); + } + + } + + @Override + public void close() { + if(null!=randomAccessFile) { + try { + randomAccessFile.close(); + } + catch(IOException ioe) { + // ignore + } + } + } + + /** + *

This gives the quantity of chunks that are in the heap.

+ * @return + */ + + private int getHeapChunkCount() { + int count = (int) (uncompressedSize / chunkSize); + + if(0!=uncompressedSize % chunkSize) { + count++; + } + + return count; + } + + private int getHeapChunkUncompressedLength(int index) { + if(index < getHeapChunkCount()-1) { + return (int) chunkSize; + } + + return (int) (uncompressedSize - (chunkSize * (getHeapChunkCount() - 1))); + } + + private int getHeapChunkCompressedLength(int index) throws IOException, HpkException { + return heapChunkCompressedLengths[index]; + } + + /** + *

After the chunk data is a whole lot of unsigned shorts that define the compressed + * size of the chunks in the heap. This method will shift the input stream to the + * start of those shorts and read them in.

+ */ + + private void populateChunkCompressedLengths(int lengths[]) throws IOException, HpkException { + Preconditions.checkNotNull(lengths); + + int count = getHeapChunkCount(); + long totalCompressedLength = 0; + randomAccessFile.seek(heapOffset + compressedSize - (2 * (count-1))); + + for(int i=0;i uncompressedSize) { + throw new HpkException( + String.format("the chunk at %d is of size %d, but the uncompressed length of the chunks is %d", + i, + lengths[i], + uncompressedSize)); + } + + totalCompressedLength += lengths[i]; + } + + // the last one will be missing will need to be derived + lengths[count-1] = (int) (compressedSize - ((2*(count-1)) + totalCompressedLength)); + + if(lengths[count-1] < 0 || lengths[count-1] > uncompressedSize) { + throw new HpkException( + String.format( + "the derivation of the last chunk size of %d is out of bounds", + lengths[count-1])); + } + + totalCompressedLength += lengths[count-1]; + } + + private boolean isHeapChunkCompressed(int index) throws IOException, HpkException { + return getHeapChunkCompressedLength(index) < getHeapChunkUncompressedLength(index); + } + + private long getHeapChunkAbsoluteFileOffset(int index) throws IOException, HpkException { + long result = heapOffset; // heap comes after the header. + + for(int i=0;iThis will read from the current offset into the supplied buffer until the supplied buffer is completely + * filledup.

+ */ + + private void readFully(byte[] buffer) throws IOException, HpkException { + Preconditions.checkNotNull(buffer); + int total = 0; + + while(total < buffer.length) { + int read = randomAccessFile.read(buffer,total,buffer.length - total); + + if(-1==read) { + throw new HpkException("unexpected end of file when reading a chunk"); + } + + total += read; + } + } + + /** + *

This will read a chunk of the heap into the supplied buffer. It is assumed that the buffer will be + * of the correct length for the uncompressed heap chunk size.

+ */ + + private void readHeapChunk(int index, byte[] buffer) throws IOException, HpkException { + + randomAccessFile.seek(getHeapChunkAbsoluteFileOffset(index)); + int chunkUncompressedLength = getHeapChunkUncompressedLength(index); + + if(isHeapChunkCompressed(index) || HeapCompression.NONE == compression) { + + switch(compression) { + case NONE: + throw new IllegalStateException(); + + case ZLIB: + { + byte[] deflatedBuffer = new byte[(int) getHeapChunkCompressedLength(index)]; + readFully(deflatedBuffer); + + Inflater inflater = new Inflater(); + inflater.setInput(deflatedBuffer); + + try { + int read; + + if(chunkUncompressedLength != (read = inflater.inflate(buffer))) { + + // the last chunk size uncompressed may be smaller than the chunk size, + // so don't throw an exception if this happens. + + if(index < getHeapChunkCount()-1) { + String message = String.format("a compressed heap chunk inflated to %d bytes; was expecting %d",read,chunkUncompressedLength); + + if(inflater.needsInput()) { + message += "; needs input"; + } + + if(inflater.needsDictionary()) { + message += "; needs dictionary"; + } + + throw new HpkException(message); + } + } + + if(!inflater.finished()) { + throw new HpkException(String.format("incomplete inflation of input data while reading chunk %d",index)); + } + } + catch(DataFormatException dfe) { + throw new HpkException("unable to inflate (decompress) heap chunk "+index,dfe); + } + } + break; + + default: + throw new IllegalStateException("unsupported compression; "+compression); + } + } + else { + int read; + + if(chunkUncompressedLength != (read = randomAccessFile.read(buffer,0,chunkUncompressedLength))) { + throw new HpkException(String.format("problem reading chunk %d of heap; only read %d of %d bytes",index,read,buffer.length)); + } + } + } + + @Override + public int readHeap(long offset) { + Preconditions.checkState(offset >= 0); + Preconditions.checkState(offset < uncompressedSize); + + int chunkIndex = (int) (offset / chunkSize); + int chunkOffset = (int) (offset - (chunkIndex * chunkSize)); + byte[] chunkData = heapChunkUncompressedCache.getUnchecked(chunkIndex); + + return chunkData[chunkOffset] & 0xff; + } + + @Override + public void readHeap(byte[] buffer, int bufferOffset, HeapCoordinates coordinates) { + + Preconditions.checkNotNull(buffer); + Preconditions.checkState(bufferOffset >= 0); + Preconditions.checkState(bufferOffset < buffer.length); + Preconditions.checkState(coordinates.getOffset() >= 0); + Preconditions.checkState(coordinates.getOffset() < uncompressedSize); + Preconditions.checkState(coordinates.getOffset()+coordinates.getLength() < uncompressedSize); + + // first figure out how much to read from this chunk + + int chunkIndex = (int) (coordinates.getOffset() / chunkSize); + int chunkOffset = (int) (coordinates.getOffset() - (chunkIndex * chunkSize)); + int chunkLength; + int chunkUncompressedLength = getHeapChunkUncompressedLength(chunkIndex); + + if(chunkOffset + coordinates.getLength() > chunkUncompressedLength) { + chunkLength = (int) (chunkUncompressedLength - chunkOffset); + } + else { + chunkLength = (int) coordinates.getLength(); + } + + // now read it in. + + byte[] chunkData = heapChunkUncompressedCache.getUnchecked(chunkIndex); + + System.arraycopy(chunkData,chunkOffset,buffer,bufferOffset,chunkLength); + + // if we need to get some more data from the next chunk then call again. + // TODO - recursive approach may not be too good when more data is involved; probably ok for hpkr though. + + if(chunkLength < coordinates.getLength()) { + readHeap( + buffer, + bufferOffset + chunkLength, + new HeapCoordinates( + heapOffset + chunkLength, + coordinates.getLength() - chunkLength)); + } + + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/Attribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/Attribute.java new file mode 100644 index 00000000..7e5aecac --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/Attribute.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.haikuos.pkg.AttributeContext; +import org.haikuos.pkg.HpkException; + +import java.util.Collections; +import java.util.List; + +/** + *

This is the superclass of the different types (data types) of attributes.

+ */ + +public abstract class Attribute { + + private AttributeId attributeId; + + private List childAttributes = null; + + public Attribute(AttributeId attributeId) { + super(); + this.attributeId = attributeId; + } + + public AttributeId getAttributeId() { + return attributeId; + } + + public abstract AttributeType getAttributeType(); + + public abstract Object getValue(AttributeContext context) throws HpkException; + + public void addChildAttribute(Attribute attribute) { + Preconditions.checkNotNull(attribute); + + if(null==childAttributes) { + childAttributes = Lists.newArrayList(); + } + + childAttributes.add(attribute); + } + + public boolean hasChildAttributes() { + return null!=childAttributes && !childAttributes.isEmpty(); + } + + public List getChildAttributes() { + if(null==childAttributes) { + return Collections.emptyList(); + } + return childAttributes; + } + + public List getChildAttributes(final AttributeId attributeId) { + Preconditions.checkNotNull(attributeId); + return Lists.newArrayList(Iterables.filter( + getChildAttributes(), + new Predicate() { + @Override + public boolean apply(Attribute input) { + return input.getAttributeId() == attributeId; + } + } + )); + } + + public Optional getChildAttribute(final AttributeId attributeId) { + Preconditions.checkNotNull(attributeId); + return Iterables.tryFind( + getChildAttributes(), + new Predicate() { + @Override + public boolean apply(Attribute input) { + return input.getAttributeId() == attributeId; + } + } + ); + } + + @Override + public String toString() { + return getAttributeId().getName() + " : " + getAttributeType().toString(); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/AttributeId.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/AttributeId.java new file mode 100644 index 00000000..c3339bbb --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/AttributeId.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +/** + *

These constants define the meaning of an {@link Attribute}. The numerical value is a value that comes up + * in the formatted file that maps to these constants. The string value is a name for the attribute and the type + * gives the type that is expected to be associated with an {@link Attribute} that has one of these IDs.

+ * + *

These constants were obtained from + * here and then the + * search/replace of B_DEFINE_HPKG_ATTRIBUTE\([ ]*(\d+),[ ]*([A-Z]+),[ \t]*("[a-z:\-.]+"),[ \t]*([A-Z_]+)\) + * / $4($1,$3,$2), was applied.

+ */ + +public enum AttributeId { + + DIRECTORY_ENTRY(0,"dir:entry", AttributeType.STRING), + FILE_TYPE(1,"file:type", AttributeType.INT), + FILE_PERMISSIONS(2,"file:permissions", AttributeType.INT), + FILE_USER(3,"file:user", AttributeType.STRING), + FILE_GROUP(4,"file:group", AttributeType.STRING), + FILE_ATIME(5,"file:atime", AttributeType.INT), + FILE_MTIME(6,"file:mtime", AttributeType.INT), + FILE_CRTIME(7,"file:crtime", AttributeType.INT), + FILE_ATIME_NANOS(8,"file:atime:nanos", AttributeType.INT), + FILE_MTIME_NANOS(9,"file:mtime:nanos", AttributeType.INT), + FILE_CRTIM_NANOS(10,"file:crtime:nanos", AttributeType.INT), + FILE_ATTRIBUTE(11,"file:attribute", AttributeType.STRING), + FILE_ATTRIBUTE_TYPE(12,"file:attribute:type", AttributeType.INT), + DATA(13,"data", AttributeType.RAW), + SYMLINK_PATH(14,"symlink:path", AttributeType.STRING), + PACKAGE_NAME(15,"package:name", AttributeType.STRING), + PACKAGE_SUMMARY(16,"package:summary", AttributeType.STRING), + PACKAGE_DESCRIPTION(17,"package:description", AttributeType.STRING), + PACKAGE_VENDOR(18,"package:vendor", AttributeType.STRING), + PACKAGE_PACKAGER(19,"package:packager", AttributeType.STRING), + PACKAGE_FLAGS(20,"package:flags", AttributeType.INT), + PACKAGE_ARCHITECTURE(21,"package:architecture", AttributeType.INT), + PACKAGE_VERSION_MAJOR(22,"package:version.major", AttributeType.STRING), + PACKAGE_VERSION_MINOR(23,"package:version.minor", AttributeType.STRING), + PACKAGE_VERSION_MICRO(24,"package:version.micro", AttributeType.STRING), + PACKAGE_VERSION_REVISION(25,"package:version.revision", AttributeType.INT), + PACKAGE_COPYRIGHT(26,"package:copyright", AttributeType.STRING), + PACKAGE_LICENSE(27,"package:license", AttributeType.STRING), + PACKAGE_PROVIDES(28,"package:provides", AttributeType.STRING), + PACKAGE_REQUIRES(29,"package:requires", AttributeType.STRING), + PACKAGE_SUPPLEMENTS(30,"package:supplements", AttributeType.STRING), + PACKAGE_CONFLICTS(31,"package:conflicts", AttributeType.STRING), + PACKAGE_FRESHENS(32,"package:freshens", AttributeType.STRING), + PACKAGE_REPLACES(33,"package:replaces", AttributeType.STRING), + PACKAGE_RESOLVABLE_OPERATOR(34,"package:resolvable.operator", AttributeType.INT), + PACKAGE_CHECKSUM(35,"package:checksum", AttributeType.STRING), + PACKAGE_VERSION_PRE_RELEASE(36,"package:version.prerelease", AttributeType.STRING), + PACKAGE_PROVIDES_COMPATIBLE(37,"package:provides.compatible", AttributeType.STRING), + PACKAGE_URL(38,"package:url", AttributeType.STRING), + PACKAGE_SOURCE_URL(39,"package:source-url", AttributeType.STRING), + PACKAGE_INSTALL_PATH(40,"package:install-path", AttributeType.STRING), + PACKAGE_BASE_PACKAGE(41,"package:base-package", AttributeType.STRING), + PACKAGE_GLOBAL_WRITABLE_FILE(42,"package:global-writable-file", AttributeType.STRING), + PACKAGE_USER_SETTINGS_FILE(43,"package:user-settings-file", AttributeType.STRING), + PACKAGE_WRITABLE_FILE_UPDATE_TYPE(44,"package:writable-file-update-type", AttributeType.INT), + PACKAGE_SETTINGS_FILE_TEMPLATE(45,"package:settings-file-template", AttributeType.STRING), + PACKAGE_USER(46,"package:user", AttributeType.STRING), + PACKAGE_USER_REAL_NAME(47,"package:user.real-name", AttributeType.STRING), + PACKAGE_USER_HOME(48,"package:user.home", AttributeType.STRING), + PACKAGE_USER_SHELL(49,"package:user.shell", AttributeType.STRING), + PACKAGE_USER_GROUP(50,"package:user.group", AttributeType.STRING), + PACKAGE_GROUP(51,"package:group", AttributeType.STRING), + PACKAGE_POST_INSTALL_SCRIPT(52,"package:post-install-script", AttributeType.STRING), + PACKAGE_IS_WRITABLE_DIRECTORY(53,"package:is-writable-directory", AttributeType.INT), + PACKAGE(54,"package", AttributeType.STRING); + + private final int code; + private final String name; + private final AttributeType attributeType; + + AttributeId(int code, String name, AttributeType attributeType) { + this.code = code; + this.name = name; + this.attributeType = attributeType; + } + + int getCode() { + return code; + } + + public String getName() { + return name; + } + + public AttributeType getAttributeType() { + return attributeType; + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/AttributeType.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/AttributeType.java new file mode 100644 index 00000000..ab3ad2d9 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/AttributeType.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +public enum AttributeType { + INT, + STRING, + RAW +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/IntAttribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/IntAttribute.java new file mode 100644 index 00000000..d5105850 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/IntAttribute.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.AttributeContext; +import org.haikuos.pkg.HpkException; + +import java.math.BigInteger; + +/** + *

This attribute is an integral numeric value. Note that the format specifies either a signed or unsigned value, + * but this concrete subclass of @{link Attribute} serves for both the signed and unsigned cases.

+ */ + +public class IntAttribute extends Attribute { + + private BigInteger numericValue; + + public IntAttribute(AttributeId attributeId, BigInteger numericValue) { + super(attributeId); + Preconditions.checkNotNull(numericValue); + this.numericValue = numericValue; + } + + @Override + public BigInteger getValue(AttributeContext context) throws HpkException { + return numericValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IntAttribute that = (IntAttribute) o; + + if (!numericValue.equals(that.numericValue)) return false; + + return true; + } + + @Override + public int hashCode() { + return numericValue.hashCode(); + } + + @Override + public AttributeType getAttributeType() { + return AttributeType.INT; + } + + @Override + public String toString() { + return String.format("%s : %s",super.toString(),numericValue.toString()); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/Pkg.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/Pkg.java new file mode 100644 index 00000000..a5ec941b --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/Pkg.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.List; + +public class Pkg { + + private String name; + private PkgVersion version; + private PkgArchitecture architecture; + private String vendor; + private List copyrights; + private List licenses; + private String summary; + private String description; + private PkgUrl homePageUrl; + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public String getVendor() { + return vendor; + } + + public void setVendor(String vendor) { + this.vendor = vendor; + } + + public PkgVersion getVersion() { + return version; + } + + public void setVersion(PkgVersion version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public PkgArchitecture getArchitecture() { + return architecture; + } + + public void setArchitecture(PkgArchitecture architecture) { + this.architecture = architecture; + } + + public List getCopyrights() { + if(null==copyrights) { + return Collections.emptyList(); + } + return copyrights; + } + + public void addCopyright(String copyright) { + Preconditions.checkNotNull(copyright); + + if(null==copyrights) { + copyrights = Lists.newArrayList(); + } + + copyrights.add(copyright); + } + + public List getLicenses() { + if(null==licenses) { + return Collections.emptyList(); + } + return licenses; + } + + public void addLicense(String license) { + Preconditions.checkNotNull(license); + + if(null==licenses) { + licenses = Lists.newArrayList(); + } + + licenses.add(license); + } + + public PkgUrl getHomePageUrl() { + return homePageUrl; + } + + public void setHomePageUrl(PkgUrl homePageUrl) { + this.homePageUrl = homePageUrl; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(null==name?"???":name); + stringBuilder.append(" : "); + stringBuilder.append(null==version?"???":version.toString()); + stringBuilder.append(" : "); + stringBuilder.append(null==architecture?"???":getArchitecture().toString()); + return stringBuilder.toString(); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgArchitecture.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgArchitecture.java new file mode 100644 index 00000000..2bebb88f --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgArchitecture.java @@ -0,0 +1,13 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +public enum PkgArchitecture { + ANY, + X86, + X86_GCC2, + SOURCE +}; diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgUrl.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgUrl.java new file mode 100644 index 00000000..5ad64bfd --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgUrl.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +public class PkgUrl { + + private String url; + + private PkgUrlType urlType; + + public PkgUrl(String url, PkgUrlType urlType) { + super(); + Preconditions.checkNotNull(urlType); + Preconditions.checkNotNull(url); + Preconditions.checkState(!Strings.isNullOrEmpty(url)); + this.url = url; + this.urlType = urlType; + } + + public PkgUrlType getUrlType() { + return urlType; + } + + public String getUrl() { + return url; + } + + @Override + public String toString() { + return String.format("%s; %s",urlType.toString(),url); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgUrlType.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgUrlType.java new file mode 100644 index 00000000..9d1cee4f --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgUrlType.java @@ -0,0 +1,10 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +public enum PkgUrlType { + HOMEPAGE +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgVersion.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgVersion.java new file mode 100644 index 00000000..a3731861 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/PkgVersion.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +public class PkgVersion { + + private String major; + private String minor; + private String micro; + private String preRelease; + private Integer revision; + + public PkgVersion(String major, String minor, String micro, String preRelease, Integer revision) { + Preconditions.checkState(!Strings.isNullOrEmpty(major)); + this.major = major; + this.minor = minor; + this.micro = micro; + this.preRelease = preRelease; + this.revision = revision; + } + + public String getMajor() { + return major; + } + + public String getMinor() { + return minor; + } + + public String getMicro() { + return micro; + } + + public String getPreRelease() { + return preRelease; + } + + public Integer getRevision() { + return revision; + } + + private void appendDotValue(StringBuilder stringBuilder, String value) { + if(null!=value) { + if(0!=stringBuilder.length()) { + stringBuilder.append('.'); + } + + stringBuilder.append(value); + } + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + appendDotValue(result,getMajor()); + appendDotValue(result,getMinor()); + appendDotValue(result,getMicro()); + appendDotValue(result, getPreRelease()); + appendDotValue(result, null == getRevision() ? null : getRevision().toString()); + return result.toString(); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawAttribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawAttribute.java new file mode 100644 index 00000000..82d31e16 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawAttribute.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +public abstract class RawAttribute extends Attribute { + + public RawAttribute(AttributeId attributeId) { + super(attributeId); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawHeapAttribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawHeapAttribute.java new file mode 100644 index 00000000..804fca8a --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawHeapAttribute.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.AttributeContext; +import org.haikuos.pkg.HpkException; +import org.haikuos.pkg.heap.HeapCoordinates; + +/** + *

This type of attribute refers to raw data. It uses coordinates into the heap to provide a source for the + * data.

+ */ + +public class RawHeapAttribute extends RawAttribute { + + private HeapCoordinates heapCoordinates; + + public RawHeapAttribute(AttributeId attributeId, HeapCoordinates heapCoordinates) { + super(attributeId); + Preconditions.checkNotNull(heapCoordinates); + this.heapCoordinates = heapCoordinates; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RawHeapAttribute that = (RawHeapAttribute) o; + + if (!heapCoordinates.equals(that.heapCoordinates)) return false; + + return true; + } + + @Override + public int hashCode() { + return heapCoordinates.hashCode(); + } + + @Override + public byte[] getValue(AttributeContext context) throws HpkException { + byte[] buffer = new byte[(int) heapCoordinates.getLength()]; + context.getHeapReader().readHeap(buffer, 0, heapCoordinates); + return buffer; + } + + @Override + public AttributeType getAttributeType() { + return AttributeType.RAW; + } + + @Override + public String toString() { + return String.format("%s : @%s",super.toString(),heapCoordinates.toString()); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawInlineAttribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawInlineAttribute.java new file mode 100644 index 00000000..c66c8970 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/RawInlineAttribute.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.AttributeContext; +import org.haikuos.pkg.HpkException; + +import java.util.Arrays; + + +public class RawInlineAttribute extends RawAttribute { + + private byte[] rawValue; + + public RawInlineAttribute(AttributeId attributeId, byte[] rawValue) { + super(attributeId); + Preconditions.checkNotNull(rawValue); + this.rawValue = rawValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RawInlineAttribute that = (RawInlineAttribute) o; + + if (!Arrays.equals(rawValue, that.rawValue)) return false; + + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(rawValue); + } + + @Override + public byte[] getValue(AttributeContext context) throws HpkException { + return rawValue; + } + + @Override + public AttributeType getAttributeType() { + return AttributeType.RAW; + } + + @Override + public String toString() { + return String.format("%s : %d b",super.toString(),rawValue.length); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringAttribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringAttribute.java new file mode 100644 index 00000000..6c16336e --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringAttribute.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +public abstract class StringAttribute extends Attribute { + + public StringAttribute(AttributeId attributeId) { + super(attributeId); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringInlineAttribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringInlineAttribute.java new file mode 100644 index 00000000..6555ce82 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringInlineAttribute.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.AttributeContext; +import org.haikuos.pkg.HpkException; + +/** + *

This type of attribute is a string. The string is supplied in the stream of attributes so this attribute will + * carry the string.

+ */ + +public class StringInlineAttribute extends StringAttribute { + + private String stringValue; + + public StringInlineAttribute(AttributeId attributeId, String stringValue) { + super(attributeId); + Preconditions.checkNotNull(stringValue); + this.stringValue = stringValue; + } + + @Override + public String getValue(AttributeContext context) throws HpkException { + return stringValue; + } + + @Override + public AttributeType getAttributeType() { + return AttributeType.STRING; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StringInlineAttribute that = (StringInlineAttribute) o; + + if (!stringValue.equals(that.stringValue)) return false; + + return true; + } + + @Override + public int hashCode() { + return stringValue.hashCode(); + } + + @Override + public String toString() { + return String.format("%s : %s",super.toString(),stringValue.toString()); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringTableRefAttribute.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringTableRefAttribute.java new file mode 100644 index 00000000..1fe017ae --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/model/StringTableRefAttribute.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.model; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.AttributeContext; +import org.haikuos.pkg.HpkException; + +/** + *

This type of attribute references a string from an instance of {@link org.haikuos.pkg.HpkStringTable} + * which is typically obtained from an instance of {@link org.haikuos.pkg.AttributeContext}.

+ */ + +public class StringTableRefAttribute extends StringAttribute { + + private int index; + + public StringTableRefAttribute(AttributeId attributeId, int index) { + super(attributeId); + Preconditions.checkState(index >= 0); + this.index = index; + } + + @Override + public String getValue(AttributeContext context) throws HpkException { + return context.getStringTable().getString(index); + } + + @Override + public AttributeType getAttributeType() { + return AttributeType.STRING; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StringTableRefAttribute that = (StringTableRefAttribute) o; + + if (index != that.index) return false; + + return true; + } + + @Override + public int hashCode() { + return index; + } + + @Override + public String toString() { + return String.format("%s : @%d",super.toString(),index); + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/output/AttributeWriter.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/output/AttributeWriter.java new file mode 100644 index 00000000..9cd28928 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/output/AttributeWriter.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.output; + +import com.google.common.base.Preconditions; +import org.haikuos.pkg.AttributeContext; +import org.haikuos.pkg.AttributeIterator; +import org.haikuos.pkg.HpkException; +import org.haikuos.pkg.model.Attribute; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +/** + *

This is a writer in the sense that it writes a human-readable dump of the attributes to a {@link Writer}. This + * enables debug or diagnostic output to be written to, for example, a file or standard output. It will write the + * attributes in an indented tree structure.

+ */ + +public class AttributeWriter extends FilterWriter { + + public AttributeWriter(Writer writer) { + super(writer); + } + + private void write(int indent, AttributeContext context, Attribute attribute) throws IOException { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(attribute); + Preconditions.checkState(indent >= 0); + + for(int i=0;iThis is a writer in the sense of being able to produce a human-readable output of a package.

+ */ + +public class PkgWriter extends FilterWriter { + + public PkgWriter(Writer writer) { + super(writer); + } + + private void write(Pkg pkg) throws IOException { + Preconditions.checkNotNull(pkg); + write(pkg.toString()); + } + + public void write(PkgIterator pkgIterator) throws IOException, HpkException { + Preconditions.checkNotNull(pkgIterator); + + try { + while(pkgIterator.hasNext()) { + write(pkgIterator.next()); + write('\n'); + } + } + catch(PkgException pe) { + throw new IOException("unable to write a package owing to an exception obtaining the package",pe); + } + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/package-info.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/package-info.java new file mode 100644 index 00000000..5aa0b698 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/package-info.java @@ -0,0 +1,11 @@ +/** + *

This package contains controller, model and helper objects for reading package files for the + * HaikuOS project. Pkg files come in two types. HPKR is a file + * format for providing a kind of catalogue of what is in a repository. HPKG format is a file that describes + * a particular package. At the time of writing, this library only supports HPKR although there is enough + * supporting material to easily provide a reader for HPKG.

+ * + *

Note that this library (currently) only supports (signed) 32bit addressing in the HPKR files.

+ */ + +package org.haikuos.pkg; \ No newline at end of file diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/tool/AttributeDumpTool.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/tool/AttributeDumpTool.java new file mode 100644 index 00000000..810dc1de --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/tool/AttributeDumpTool.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.tool; + +import org.haikuos.pkg.HpkException; +import org.haikuos.pkg.HpkrFileExtractor; +import org.haikuos.pkg.output.AttributeWriter; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; + +/** + *

Given an HPKR file, this small program will dump all of the attributes of the HPKR file. This is handy for + * diagnostic purposes.

+ */ + +public class AttributeDumpTool implements Runnable { + + protected static Logger logger = LoggerFactory.getLogger(AttributeDumpTool.class); + + @Option(name = "-f", required = true, usage = "the HPKR file is required") + private File hpkrFile; + + public static void main(String[] args) throws HpkException, IOException { + AttributeDumpTool main = new AttributeDumpTool(); + CmdLineParser parser = new CmdLineParser(main); + + try { + parser.parseArgument(args); + main.run(); + } + catch(CmdLineException cle) { + throw new IllegalStateException("unable to parse arguments",cle); + } + } + + public void run() { + CmdLineParser parser = new CmdLineParser(this); + + HpkrFileExtractor hpkrFileExtractor = null; + + try { + hpkrFileExtractor = new HpkrFileExtractor(hpkrFile); + + OutputStreamWriter streamWriter = new OutputStreamWriter(System.out); + AttributeWriter attributeWriter = new AttributeWriter(streamWriter); + attributeWriter.write(hpkrFileExtractor.getPackageAttributesIterator()); + attributeWriter.flush(); + } + catch(Throwable th) { + logger.error("unable to dump attributes",th); + } + finally { + if(null!=hpkrFileExtractor) { + hpkrFileExtractor.close(); + } + } + + } + +} diff --git a/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/tool/PkgDumpTool.java b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/tool/PkgDumpTool.java new file mode 100644 index 00000000..3d9d2e45 --- /dev/null +++ b/haikudepotserver-packagefile/src/main/java/org/haikuos/pkg/tool/PkgDumpTool.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg.tool; + +import org.haikuos.pkg.HpkException; +import org.haikuos.pkg.HpkrFileExtractor; +import org.haikuos.pkg.PkgIterator; +import org.haikuos.pkg.output.PkgWriter; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; + +/** + *

This small tool will take an HPKR file and parse it first into attributes and then into packages. The packages + * are dumped out to the standard output. This is useful as means of debugging.

+ */ + +public class PkgDumpTool { + + protected static Logger logger = LoggerFactory.getLogger(AttributeDumpTool.class); + + @Option(name = "-f", required = true, usage = "the HPKR file is required") + private File hpkrFile; + + public static void main(String[] args) throws HpkException, IOException { + PkgDumpTool main = new PkgDumpTool(); + CmdLineParser parser = new CmdLineParser(main); + + try { + parser.parseArgument(args); + main.run(); + } + catch(CmdLineException cle) { + throw new IllegalStateException("unable to parse arguments",cle); + } + } + + public void run() { + CmdLineParser parser = new CmdLineParser(this); + + HpkrFileExtractor hpkrFileExtractor = null; + + try { + hpkrFileExtractor = new HpkrFileExtractor(hpkrFile); + + OutputStreamWriter streamWriter = new OutputStreamWriter(System.out); + PkgWriter pkgWriter = new PkgWriter(streamWriter); + pkgWriter.write(new PkgIterator(hpkrFileExtractor.getPackageAttributesIterator())); + pkgWriter.flush(); + } + catch(Throwable th) { + logger.error("unable to dump packages",th); + } + finally { + if(null!=hpkrFileExtractor) { + hpkrFileExtractor.close(); + } + } + + } + + + +} diff --git a/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/AbstractHpkrTest.java b/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/AbstractHpkrTest.java new file mode 100644 index 00000000..0ea37f35 --- /dev/null +++ b/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/AbstractHpkrTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import com.google.common.io.ByteStreams; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public abstract class AbstractHpkrTest { + + /** + *

This will copy the supplied test classpath resource into a temporary file to work with during the test. It + * is the responsibility of the caller to clean up the temporary file afterwards.

+ */ + + protected File prepareTestFile() throws IOException { + InputStream inputStream = getClass().getResourceAsStream("/repo.hpkr"); + + if(null==inputStream) { + throw new IllegalStateException("unable to find the test hpkr resource"); + } + + File temporaryFile = File.createTempFile("repo-test-","hpkr"); + FileOutputStream fileOutputStream = null; + + try { + fileOutputStream = new FileOutputStream(temporaryFile); + ByteStreams.copy(inputStream, fileOutputStream); + } + catch(IOException ioe) { + temporaryFile.delete(); + throw new IOException("unable to copy the test hpkr resource to a temporary file; "+temporaryFile.getAbsolutePath()); + } + finally { + if(null!=fileOutputStream) { + try { + fileOutputStream.close(); + } + catch(IOException ioe) { + // ignore + } + } + } + + return temporaryFile; + } + +} diff --git a/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/HpkrFileExtractorAttributeTest.java b/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/HpkrFileExtractorAttributeTest.java new file mode 100644 index 00000000..ff2a89cd --- /dev/null +++ b/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/HpkrFileExtractorAttributeTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import org.haikuos.pkg.model.Attribute; +import org.haikuos.pkg.model.AttributeId; +import org.junit.Test; + +import java.io.File; +import java.io.OutputStreamWriter; +import java.math.BigInteger; + +import static org.fest.assertions.Assertions.assertThat; + +/** + *

This is a simplistic test that is just going to stream out some attributes from a known HPKR file and will + * then look for certain packages and make sure that an artificially created attribute set matches. It's basically + * a smoke test.

+ */ + +public class HpkrFileExtractorAttributeTest extends AbstractHpkrTest { + + @Test + public void testRepo() throws Exception { + + File hpkrFile = null; + HpkrFileExtractor hpkrFileExtractor = null; + Attribute ncursesSourceAttribute = null; + + try { + hpkrFile = prepareTestFile(); + hpkrFileExtractor = new HpkrFileExtractor(hpkrFile); + + OutputStreamWriter streamWriter = new OutputStreamWriter(System.out); + AttributeIterator attributeIterator = hpkrFileExtractor.getPackageAttributesIterator(); + AttributeContext attributeContext = hpkrFileExtractor.getAttributeContext(); + + while(attributeIterator.hasNext()) { + Attribute attribute = attributeIterator.next(); + + if(AttributeId.PACKAGE == attribute.getAttributeId()) { + String packageName = attribute.getValue(attributeIterator.getContext()).toString(); + + if(packageName.equals("ncurses_source")) { + ncursesSourceAttribute = attribute; + } + } + } + + // now the analysis phase. + + assertThat(ncursesSourceAttribute).isNotNull(); + assertThat(ncursesSourceAttribute.getChildAttribute(AttributeId.PACKAGE_NAME).get().getValue(attributeContext)).isEqualTo("ncurses_source"); + assertThat(ncursesSourceAttribute.getChildAttribute(AttributeId.PACKAGE_ARCHITECTURE).get().getValue(attributeContext)).isEqualTo(new BigInteger("3")); + assertThat(ncursesSourceAttribute.getChildAttribute(AttributeId.PACKAGE_URL).get().getValue(attributeContext)).isEqualTo("http://www.gnu.org/software/ncurses/ncurses.html"); + assertThat(ncursesSourceAttribute.getChildAttribute(AttributeId.PACKAGE_SOURCE_URL).get().getValue(attributeContext)).isEqualTo("Download "); + assertThat(ncursesSourceAttribute.getChildAttribute(AttributeId.PACKAGE_CHECKSUM).get().getValue(attributeContext)).isEqualTo("6a25c52890e7d335247bd96965b5cac2f04dafc1de8d12ad73346ed79f3f4215"); + + // check the version which is a sub-tree of attributes. + + Attribute majorVersionAttribute = ncursesSourceAttribute.getChildAttribute(AttributeId.PACKAGE_VERSION_MAJOR).get(); + assertThat(majorVersionAttribute.getValue(attributeContext)).isEqualTo("5"); + assertThat(majorVersionAttribute.getChildAttribute(AttributeId.PACKAGE_VERSION_MINOR).get().getValue(attributeContext)).isEqualTo("9"); + assertThat(majorVersionAttribute.getChildAttribute(AttributeId.PACKAGE_VERSION_REVISION).get().getValue(attributeContext)).isEqualTo(new BigInteger("10")); + + } + finally { + if(null!=hpkrFileExtractor) { + hpkrFileExtractor.close(); + } + + if(null!=hpkrFile) { + hpkrFile.delete(); + } + } + + } + +} diff --git a/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/PkgFactoryTest.java b/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/PkgFactoryTest.java new file mode 100644 index 00000000..0625451f --- /dev/null +++ b/haikudepotserver-packagefile/src/test/java/org/haikuos/pkg/PkgFactoryTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.pkg; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import org.haikuos.pkg.model.*; +import org.junit.Test; + +import java.math.BigInteger; + +import static org.fest.assertions.Assertions.assertThat; + +/** + *

This is a very simplistic test that checks that attributes are able to be converted into package DTO model + * objects correctly.

+ */ + +public class PkgFactoryTest { + + private Attribute createTestPackageAttributes() { + + StringInlineAttribute topA = new StringInlineAttribute(AttributeId.PACKAGE,"testpkg"); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_NAME,"testpkg")); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_VENDOR,"Test Vendor")); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_SUMMARY,"This is a test package summary")); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_DESCRIPTION,"This is a test package description")); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_URL,"http://www.haiku-os.org")); + topA.addChildAttribute(new IntAttribute(AttributeId.PACKAGE_ARCHITECTURE,new BigInteger("1"))); // X86 + + StringInlineAttribute majorVersionA = new StringInlineAttribute(AttributeId.PACKAGE_VERSION_MAJOR,"6"); + majorVersionA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_VERSION_MINOR,"32")); + majorVersionA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_VERSION_MICRO,"9")); + majorVersionA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_VERSION_PRE_RELEASE,"beta")); + majorVersionA.addChildAttribute(new IntAttribute(AttributeId.PACKAGE_VERSION_REVISION,new BigInteger("8"))); + topA.addChildAttribute(majorVersionA); + + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_COPYRIGHT,"Some copyright A")); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_COPYRIGHT,"Some copyright B")); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_LICENSE,"Some license A")); + topA.addChildAttribute(new StringInlineAttribute(AttributeId.PACKAGE_LICENSE,"Some license B")); + + return topA; + } + + @Test + public void testCreatePackage() throws PkgException { + + Attribute attribute = createTestPackageAttributes(); + PkgFactory pkgFactory = new PkgFactory(); + + // it is ok that this is empty because the factory should not need to reference the heap again; its all inline + // and we are not testing the heap here. + AttributeContext attributeContext = new AttributeContext(); + + Pkg pkg = pkgFactory.createPackage( + attributeContext, + attribute); + + // now do some checks. + + assertThat(pkg.getArchitecture()).isEqualTo(PkgArchitecture.X86); + assertThat(pkg.getName()).isEqualTo("testpkg"); + assertThat(pkg.getVendor()).isEqualTo("Test Vendor"); + assertThat(pkg.getSummary()).isEqualTo("This is a test package summary"); + assertThat(pkg.getDescription()).isEqualTo("This is a test package description"); + assertThat(pkg.getHomePageUrl().getUrl()).isEqualTo("http://www.haiku-os.org"); + assertThat(pkg.getHomePageUrl().getUrlType()).isEqualTo(PkgUrlType.HOMEPAGE); + + assertThat(pkg.getVersion().getMajor()).isEqualTo("6"); + assertThat(pkg.getVersion().getMinor()).isEqualTo("32"); + assertThat(pkg.getVersion().getMicro()).isEqualTo("9"); + assertThat(pkg.getVersion().getPreRelease()).isEqualTo("beta"); + assertThat(pkg.getVersion().getRevision()).isEqualTo(8); + + assertThat(Iterables.tryFind( + pkg.getCopyrights(), + new Predicate() { + @Override + public boolean apply(java.lang.String input) { + return input.equals("Some copyright A"); + } + }).isPresent()).isTrue(); + + assertThat(Iterables.tryFind( + pkg.getCopyrights(), + new Predicate() { + @Override + public boolean apply(java.lang.String input) { + return input.equals("Some copyright B"); + } + }).isPresent()).isTrue(); + + assertThat(Iterables.tryFind( + pkg.getLicenses(), + new Predicate() { + @Override + public boolean apply(java.lang.String input) { + return input.equals("Some license A"); + } + }).isPresent()).isTrue(); + + assertThat(Iterables.tryFind( + pkg.getLicenses(), + new Predicate() { + @Override + public boolean apply(java.lang.String input) { + return input.equals("Some license B"); + } + }).isPresent()).isTrue(); + + } + +} diff --git a/haikudepotserver-packagefile/src/test/resources/README.TXT b/haikudepotserver-packagefile/src/test/resources/README.TXT new file mode 100644 index 00000000..ca8c31c0 --- /dev/null +++ b/haikudepotserver-packagefile/src/test/resources/README.TXT @@ -0,0 +1,5 @@ +The file "repo.hpkr" was obtained from; + +http://haiku-files.org/files/repo/9818164862edcbf69404a90267090b9d595908a11941951e904dfc6244c3d566/repo + +2013-09-30 \ No newline at end of file diff --git a/haikudepotserver-packagefile/src/test/resources/repo.hpkr b/haikudepotserver-packagefile/src/test/resources/repo.hpkr new file mode 100644 index 00000000..62b89933 Binary files /dev/null and b/haikudepotserver-packagefile/src/test/resources/repo.hpkr differ diff --git a/haikudepotserver-parent/pom.xml b/haikudepotserver-parent/pom.xml new file mode 100644 index 00000000..d5e483b8 --- /dev/null +++ b/haikudepotserver-parent/pom.xml @@ -0,0 +1,157 @@ + + + 4.0.0 + org.haikuos + haikudepotserver-parent + pom + 1.0.1-SNAPSHOT + + + 3.2.4.RELEASE + UTF-8 + + + + + jsonrpc4j-webdav-maven-repo + jsonrpc4j maven repository + http://jsonrpc4j.googlecode.com/svn/maven/repo/ + default + + + + + + + + args4j + args4j + 2.0.23 + + + + + org.slf4j + slf4j-api + 1.7.5 + + + ch.qos.logback + logback-classic + 1.0.12 + + + org.slf4j + jul-to-slf4j + 1.7.5 + + + org.slf4j + jcl-over-slf4j + 1.7.5 + + + + + com.google.guava + guava + 14.0.1 + + + + + jstl + jstl + 1.2 + + + javax.servlet + javax.servlet-api + 3.0.1 + + + com.googlecode + jsonrpc4j + 0.28 + + + javax.servlet.jsp + jsp-api + 2.2 + + + + + com.googlecode.flyway + flyway-core + 2.2.1 + + + org.apache.cayenne + cayenne-server + 3.1M3 + + + + + org.apache.tomcat + tomcat-jdbc + 7.0.39 + + + + + org.springframework + spring-core + ${spring.version} + + + org.springframework + spring-web + ${spring.version} + + + org.springframework + spring-webmvc + ${spring.version} + + + org.springframework + spring-test + ${spring.version} + + + + + junit + junit + 4.8.2 + + + org.easytesting + fest-assert + 1.4 + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.6 + 1.6 + + + + + + + + diff --git a/haikudepotserver-webapp/pom.xml b/haikudepotserver-webapp/pom.xml new file mode 100644 index 00000000..205c239d --- /dev/null +++ b/haikudepotserver-webapp/pom.xml @@ -0,0 +1,174 @@ + + + + haikudepotserver-parent + org.haikuos + ../haikudepotserver-parent + 1.0.1-SNAPSHOT + + + 4.0.0 + org.haikuos + haikudepotserver-webapp + war + + + + 0.6.0 + 0.4.0 + 1.2.0 + 0.1.3 + 1.5.2 + 2.3.1 + + + http://code.angularjs.org + http://cdnjs.cloudflare.com/ajax/libs + ${basedir}/src/main/webapp/js/lib + ${basedir}/src/main/webapp/bootstrap + + + + + + + + org.haikuos + haikudepotserver-packagefile + ${project.version} + + + org.haikuos + haikudepotserver-api1 + ${project.version} + + + + + com.google.guava + guava + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jul-to-slf4j + + + org.slf4j + jcl-over-slf4j + + + + + jstl + jstl + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet.jsp + jsp-api + provided + + + + + com.googlecode.flyway + flyway-core + + + org.apache.cayenne + cayenne-server + + + + + org.apache.tomcat + tomcat-jdbc + + + + + org.springframework + spring-core + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + + + org.springframework + spring-test + test + + + + junit + junit + test + + + + + + + + + + + + maven-antrun-plugin + 1.7 + + + generate-sources + + + + + + + run + + + + + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.1 + + / + + + + postgresql + postgresql + 9.1-901.jdbc4 + + + + + + + + diff --git a/haikudepotserver-webapp/src/etc/ant/fetchwebresources-build.xml b/haikudepotserver-webapp/src/etc/ant/fetchwebresources-build.xml new file mode 100644 index 00000000..d99e941b --- /dev/null +++ b/haikudepotserver-webapp/src/etc/ant/fetchwebresources-build.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/CaptchaApiImpl.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/CaptchaApiImpl.java new file mode 100644 index 00000000..5224d01d --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/CaptchaApiImpl.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import org.haikuos.haikudepotserver.api1.model.captcha.GenerateCaptchaRequest; +import org.haikuos.haikudepotserver.api1.model.captcha.GenerateCaptchaResult; +import org.haikuos.haikudepotserver.captcha.CaptchaService; +import org.haikuos.haikudepotserver.captcha.model.Captcha; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +public class CaptchaApiImpl implements CaptchaApi { + + @Resource + CaptchaService captchaService; + + @Override + public GenerateCaptchaResult generateCaptcha(GenerateCaptchaRequest generateCaptchaRequest) { + Preconditions.checkNotNull(generateCaptchaRequest); + + Captcha captcha = captchaService.generate(); + + GenerateCaptchaResult result = new GenerateCaptchaResult(); + result.token = captcha.getToken(); + result.pngImageDataBase64 = BaseEncoding.base64().encode(captcha.getPngImageData()); + return result; + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java new file mode 100644 index 00000000..7b1d128e --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllArchitecturesRequest; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllArchitecturesResult; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllMessagesRequest; +import org.haikuos.haikudepotserver.api1.model.miscellaneous.GetAllMessagesResult; +import org.haikuos.haikudepotserver.model.Architecture; +import org.haikuos.haikudepotserver.support.Closeables; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Properties; + +@Component +public class MiscellaneousApiImpl implements MiscellaneousApi { + + public final static String RESOURCE_MESSAGES = "/messages.properties"; + + @Resource + ServerRuntime serverRuntime; + + @Override + public GetAllArchitecturesResult getAllArchitectures(GetAllArchitecturesRequest getAllArchitecturesRequest) { + Preconditions.checkNotNull(getAllArchitecturesRequest); + GetAllArchitecturesResult result = new GetAllArchitecturesResult(); + result.architectures = + Lists.newArrayList( + Iterables.transform( + + // we want to explicitly exclude 'source' and 'any' because they are pseudo + // architectures. + + Iterables.filter( + Architecture.getAll(serverRuntime.getContext()), + new Predicate() { + @Override + public boolean apply(org.haikuos.haikudepotserver.model.Architecture input) { + return + !input.getCode().equals(Architecture.CODE_SOURCE) + && !input.getCode().equals(Architecture.CODE_ANY); + } + } + ), + new Function() { + @Override + public GetAllArchitecturesResult.Architecture apply(org.haikuos.haikudepotserver.model.Architecture input) { + GetAllArchitecturesResult.Architecture result = new GetAllArchitecturesResult.Architecture(); + result.code = input.getCode(); + return result; + } + } + ) + ); + + return result; + } + + @Override + public GetAllMessagesResult getAllMessages(GetAllMessagesRequest getAllMessagesRequest) { + Preconditions.checkNotNull(getAllMessagesRequest); + + InputStream inputStream = null; + + try { + inputStream = getClass().getResourceAsStream(RESOURCE_MESSAGES); + + if(null==inputStream) { + throw new FileNotFoundException(RESOURCE_MESSAGES); + } + + Properties properties = new Properties(); + properties.load(inputStream); + Map map = Maps.newHashMap(); + + for(String propertyName : properties.stringPropertyNames()) { + map.put(propertyName, properties.get(propertyName).toString()); + } + + GetAllMessagesResult getAllMessagesResult = new GetAllMessagesResult(); + getAllMessagesResult.messages = map; + return getAllMessagesResult; + } + catch(IOException ioe) { + throw new RuntimeException("unable to assemble the messages to send for api1",ioe); + } + finally { + Closeables.closeQuietly(inputStream); + } + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java new file mode 100644 index 00000000..1f310338 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/PkgApiImpl.java @@ -0,0 +1,232 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.haikuos.haikudepotserver.api1.model.pkg.GetPkgRequest; +import org.haikuos.haikudepotserver.api1.model.pkg.GetPkgResult; +import org.haikuos.haikudepotserver.api1.model.pkg.SearchPkgsRequest; +import org.haikuos.haikudepotserver.api1.model.pkg.SearchPkgsResult; +import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException; +import org.haikuos.haikudepotserver.model.*; +import org.haikuos.haikudepotserver.services.SearchPkgsService; +import org.haikuos.haikudepotserver.services.model.SearchPkgsSpecification; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; + +/** + *

See {@link PkgApi} for details on the methods this API affords.

+ */ + +@Component +public class PkgApiImpl implements PkgApi { + + @Resource + ServerRuntime serverRuntime; + + @Resource + SearchPkgsService searchPkgsService; + + @Override + public SearchPkgsResult searchPkgs(SearchPkgsRequest request) { + Preconditions.checkNotNull(request); + Preconditions.checkState(!Strings.isNullOrEmpty(request.architectureCode)); + + final ObjectContext context = serverRuntime.getContext(); + + SearchPkgsSpecification specification = new SearchPkgsSpecification(); + + String exp = request.expression; + + if(null!=exp) { + exp = Strings.emptyToNull(exp.trim().toLowerCase()); + } + + specification.setExpression(exp); + + if(null!=request.expressionType) { + specification.setExpressionType( + SearchPkgsSpecification.ExpressionType.valueOf(request.expressionType.name())); + } + + final Optional architectureOptional = Architecture.getByCode(context,request.architectureCode); + + if(!architectureOptional.isPresent()) { + throw new IllegalStateException("the architecture specified is not able to be found; "+request.architectureCode); + } + + specification.setArchitectures(Sets.newHashSet( + architectureOptional.get() + , + Architecture.getByCode(context,Architecture.CODE_ANY).get() +// , +// Architecture.getByCode(context,Architecture.CODE_SOURCE).get() + )); + + specification.setLimit(request.limit+1); // get +1 to see if there are any more. + specification.setOffset(request.offset); + + SearchPkgsResult result = new SearchPkgsResult(); + List searchedPkgs = searchPkgsService.search(context,specification); + + // if there are more than we asked for then there must be more available. + + result.hasMore = new Boolean(searchedPkgs.size() > request.limit); + + if(result.hasMore) { + searchedPkgs = searchedPkgs.subList(0,request.limit); + } + + result.pkgs = Lists.newArrayList(Iterables.transform( + searchedPkgs, + new Function() { + @Override + public SearchPkgsResult.Pkg apply(org.haikuos.haikudepotserver.model.Pkg input) { + SearchPkgsResult.Pkg resultPkg = new SearchPkgsResult.Pkg(); + resultPkg.name = input.getName(); + + Optional pkgVersionOptional = PkgVersion.getLatestForPkg( + context, + input, + Lists.newArrayList( + architectureOptional.get(), + Architecture.getByCode(context, Architecture.CODE_ANY).get(), + Architecture.getByCode(context, Architecture.CODE_SOURCE).get())); + + if(pkgVersionOptional.isPresent()) { + SearchPkgsResult.Version resultVersion = new SearchPkgsResult.Version(); + resultVersion.major = pkgVersionOptional.get().getMajor(); + resultVersion.minor = pkgVersionOptional.get().getMinor(); + resultVersion.micro = pkgVersionOptional.get().getMicro(); + resultVersion.preRelease = pkgVersionOptional.get().getPreRelease(); + resultVersion.revision = pkgVersionOptional.get().getRevision(); + resultPkg.version = resultVersion; + } + + return resultPkg; + } + } + )); + + + return result; + } + + /** + *

Given the persistence model object, this method will construct the DTO to be sent back over the wire.

+ */ + + private GetPkgResult.Version createVersion(PkgVersion pkgVersion) { + GetPkgResult.Version version = new GetPkgResult.Version(); + + version.major = pkgVersion.getMajor(); + version.minor = pkgVersion.getMinor(); + version.micro = pkgVersion.getMicro(); + version.revision = pkgVersion.getRevision(); + version.preRelease = pkgVersion.getPreRelease(); + + version.architectureCode = pkgVersion.getArchitecture().getCode(); + version.copyrights = Lists.transform( + pkgVersion.getPkgVersionCopyrights(), + new Function() { + @Override + public String apply(org.haikuos.haikudepotserver.model.PkgVersionCopyright input) { + return input.getBody(); + } + }); + + version.licenses = Lists.transform( + pkgVersion.getPkgVersionLicenses(), + new Function() { + @Override + public String apply(org.haikuos.haikudepotserver.model.PkgVersionLicense input) { + return input.getBody(); + } + }); + + // TODO - languages + if(!pkgVersion.getPkgVersionLocalizations().isEmpty()) { + PkgVersionLocalization pkgVersionLocalization = pkgVersion.getPkgVersionLocalizations().get(0); + version.description = pkgVersionLocalization.getDescription(); + version.summary = pkgVersionLocalization.getSummary(); + } + + version.urls = Lists.transform( + pkgVersion.getPkgVersionUrls(), + new Function() { + @Override + public GetPkgResult.Url apply(org.haikuos.haikudepotserver.model.PkgVersionUrl input) { + GetPkgResult.Url url = new GetPkgResult.Url(); + url.url = input.getUrl(); + url.urlTypeCode = input.getPkgUrlType().getCode(); + return url; + } + }); + + return version; + } + + @Override + public GetPkgResult getPkg(GetPkgRequest request) throws ObjectNotFoundException { + + Preconditions.checkNotNull(request); + Preconditions.checkState(!Strings.isNullOrEmpty(request.name)); + Preconditions.checkState(!Strings.isNullOrEmpty(request.architectureCode)); + Preconditions.checkNotNull(request.versionType); + + final ObjectContext context = serverRuntime.getContext(); + + Optional architectureOptional = Architecture.getByCode(context, request.architectureCode); + + if(!architectureOptional.isPresent()) { + throw new IllegalStateException("the specified architecture was not able to be found; "+request.architectureCode); + } + + GetPkgResult result = new GetPkgResult(); + Optional pkgOptional = Pkg.getByName(context, request.name); + + if(!pkgOptional.isPresent()) { + throw new ObjectNotFoundException(Pkg.class.getSimpleName(), request.name); + } + + result.name = pkgOptional.get().getName(); + + switch(request.versionType) { + case LATEST: + Optional pkgVersionOptional = PkgVersion.getLatestForPkg( + context, + pkgOptional.get(), + Lists.newArrayList( + architectureOptional.get(), + Architecture.getByCode(context, Architecture.CODE_ANY).get(), + Architecture.getByCode(context, Architecture.CODE_SOURCE).get())); + + if(!pkgVersionOptional.isPresent()) { + throw new ObjectNotFoundException(PkgVersion.class.getSimpleName(), request.name); + } + + result.versions = Collections.singletonList(createVersion(pkgVersionOptional.get())); + break; + + default: + throw new IllegalStateException("unhandled version type in request"); + } + + return result; + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java new file mode 100644 index 00000000..cb2e613f --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.Uninterruptibles; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.haikuos.haikudepotserver.api1.model.user.*; +import org.haikuos.haikudepotserver.api1.support.CaptchaBadResponseException; +import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException; +import org.haikuos.haikudepotserver.captcha.CaptchaService; +import org.haikuos.haikudepotserver.model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Component +public class UserApiImpl implements UserApi { + + protected static Logger logger = LoggerFactory.getLogger(UserApiImpl.class); + + @Resource + ServerRuntime serverRuntime; + + @Resource + CaptchaService captchaService; + + @Override + public CreateUserResult createUser(CreateUserRequest createUserRequest) { + + Preconditions.checkNotNull(createUserRequest); + Preconditions.checkNotNull(createUserRequest.captchaToken); + Preconditions.checkNotNull(createUserRequest.captchaResponse); + + // check the supplied catcha matches the token. + + if(!captchaService.verify(createUserRequest.captchaToken, createUserRequest.captchaResponse)) { + throw new CaptchaBadResponseException(); + } + + // we need to check the nickname even before we create the user because we have to + // check for uniqueness of the nickname across all of the users. + + if(Strings.isNullOrEmpty(createUserRequest.nickname)) { + throw new org.haikuos.haikudepotserver.api1.support.ValidationException( + new org.haikuos.haikudepotserver.api1.support.ValidationFailure( + User.NICKNAME_PROPERTY,"required") + ); + } + + final ObjectContext context = serverRuntime.getContext(); + + //need to check that the nickname is not already in use. + + if(User.getByNickname(context,createUserRequest.nickname).isPresent()) { + throw new org.haikuos.haikudepotserver.api1.support.ValidationException( + new org.haikuos.haikudepotserver.api1.support.ValidationFailure( + User.NICKNAME_PROPERTY,"notunique") + ); + } + + User user = context.newObject(User.class); + user.setNickname(createUserRequest.nickname); + user.setPasswordSalt(); // random + user.setPasswordHash(Hashing.sha256().hashString(user.getPasswordSalt() + createUserRequest.passwordClear).toString()); + context.commitChanges(); + + logger.info("data create user; {}",user.getNickname()); + + return new CreateUserResult(); + } + + @Override + public GetUserResult getUser(GetUserRequest getUserRequest) throws ObjectNotFoundException { + Preconditions.checkNotNull(getUserRequest); + Preconditions.checkState(!Strings.isNullOrEmpty(getUserRequest.nickname)); + + final ObjectContext context = serverRuntime.getContext(); + + Optional user = User.getByNickname(context, getUserRequest.nickname); + + if(!user.isPresent()) { + throw new ObjectNotFoundException(User.class.getSimpleName(), User.NICKNAME_PROPERTY); + } + + GetUserResult result = new GetUserResult(); + result.nickname = user.get().getNickname(); + return result; + } + + // TODO; some sort of brute-force checking here; too many authentication requests in a short period; go into lock-down? + + @Override + public AuthenticateUserResult authenticateUser(AuthenticateUserRequest authenticateUserRequest) { + Preconditions.checkNotNull(authenticateUserRequest); + AuthenticateUserResult authenticateUserResult = new AuthenticateUserResult(); + authenticateUserResult.authenticated = false; + + if(null!=authenticateUserRequest.nickname) { + authenticateUserRequest.nickname = authenticateUserRequest.nickname.trim(); + } + + if(null!=authenticateUserRequest.passwordClear) { + authenticateUserRequest.passwordClear = authenticateUserRequest.passwordClear.trim(); + } + + if( + !Strings.isNullOrEmpty(authenticateUserRequest.nickname) + && !Strings.isNullOrEmpty(authenticateUserRequest.passwordClear)) { + + final ObjectContext context = serverRuntime.getContext(); + + Optional userOptional = User.getByNickname(context, authenticateUserRequest.nickname); + + if(userOptional.isPresent()) { + String saltAndPasswordClear = userOptional.get().getPasswordSalt() + authenticateUserRequest.passwordClear; + String inboundHash = Hashing.sha256().hashString(saltAndPasswordClear).toString(); + authenticateUserResult.authenticated = inboundHash.equals(userOptional.get().getPasswordHash()); + } + } + + if(!authenticateUserResult.authenticated) { + Uninterruptibles.sleepUninterruptibly(5,TimeUnit.SECONDS); + } + + return authenticateUserResult; + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ErrorResolverImpl.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ErrorResolverImpl.java new file mode 100644 index 00000000..5ac301ea --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ErrorResolverImpl.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.support; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.googlecode.jsonrpc4j.DefaultErrorResolver; +import com.googlecode.jsonrpc4j.ErrorResolver; +import org.apache.cayenne.validation.BeanValidationFailure; +import org.apache.cayenne.validation.SimpleValidationFailure; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +/** + *

This class is able to take exceptions and throwables and turn them into valid JSON-RPC errors that can be + * returned to the client with specific codes and with specific data.

+ */ + +public class ErrorResolverImpl implements ErrorResolver { + + @Override + public JsonError resolveError(Throwable t, Method method, List arguments) { + + // special output for a bad captcha + + if(CaptchaBadResponseException.class.isAssignableFrom(t.getClass())) { + return new JsonError( + Constants.ERROR_CODE_CAPTCHABADRESPONSE, + "captchabadresponse", + null); + } + + // special output for the object not found exceptions + + if(ObjectNotFoundException.class.isAssignableFrom(t.getClass())) { + ObjectNotFoundException objectNotFoundException = (ObjectNotFoundException) t; + + return new JsonError( + Constants.ERROR_CODE_OBJECTNOTFOUND, + "objectnotfound", + ImmutableMap.of( + "entityname", objectNotFoundException.getEntityName(), + "identifier", objectNotFoundException.getIdentifier() + ) + ); + } + + // special output for the validation exceptions + + if(ValidationException.class.isAssignableFrom(t.getClass())) { + ValidationException validationException = (ValidationException) t; + + return new JsonError( + Constants.ERROR_CODE_VALIDATION, + "validationerror", + ImmutableMap.of( + "validationfailures", + Lists.transform( + validationException.getValidationFailures(), + new Function() { + @Override + public Map apply(org.haikuos.haikudepotserver.api1.support.ValidationFailure input) { + return ImmutableMap.of( + "property",input.getProperty(), + "message",input.getMessage() + ); + } + } + ) + ) + ); + } + + // special output for cayenne validation exceptions + + if(org.apache.cayenne.validation.ValidationException.class.isAssignableFrom(t.getClass())) { + org.apache.cayenne.validation.ValidationException validationException = (org.apache.cayenne.validation.ValidationException) t; + + return new JsonError( + Constants.ERROR_CODE_VALIDATION, + "validationerror", + ImmutableMap.of( + "validationfailures", + Lists.transform( + validationException.getValidationResult().getFailures(), + new Function() { + @Override + public Map apply(org.apache.cayenne.validation.ValidationFailure input) { + if(BeanValidationFailure.class.isAssignableFrom(input.getClass())) { + BeanValidationFailure beanValidationFailure = (BeanValidationFailure) input; + return ImmutableMap.of( + "property", beanValidationFailure.getProperty(), + "message", beanValidationFailure.toString()); + } + + if(SimpleValidationFailure.class.isAssignableFrom(input.getClass())) { + SimpleValidationFailure simpleValidationFailure = (SimpleValidationFailure) input; + return ImmutableMap.of( + "property", "", + "message", simpleValidationFailure.getDescription()); + } + + throw new IllegalStateException("unable to establish data portion of validation exception owing to unknown cayenne validation failure; "+input.getClass().getSimpleName()); + } + } + ) + ) + ); + } + + return DefaultErrorResolver.INSTANCE.resolveError(t,method,arguments); + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ObjectMapperFactory.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ObjectMapperFactory.java new file mode 100644 index 00000000..5874647a --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/support/ObjectMapperFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.support; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; + +/** + *

The JSON handling class needs some special configuration in order for it to, for example, ignore + * missing fields on objects. This factory is able to create those + * {@link ObjectMapper} objects correctly configured ready for use.

+ */ + +public class ObjectMapperFactory implements FactoryBean { + + @Override + public ObjectMapper getObject() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + return objectMapper; + } + + @Override + public Class getObjectType() { + return ObjectMapper.class; + } + + @Override + public boolean isSingleton() { + return false; + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/CaptchaService.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/CaptchaService.java new file mode 100644 index 00000000..900cc34b --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/CaptchaService.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.captcha; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import org.haikuos.haikudepotserver.captcha.model.Captcha; +import org.haikuos.haikudepotserver.captcha.model.CaptchaAlgorithm; +import org.haikuos.haikudepotserver.captcha.model.CaptchaRepository; + +/** + *

This service is able to provide interfacing to the captcha system including verification and generation of + * captchas.

+ */ + +public class CaptchaService { + + private CaptchaAlgorithm captchaAlgorithm; + private CaptchaRepository captchaRepository; + + public CaptchaAlgorithm getCaptchaAlgorithm() { + return captchaAlgorithm; + } + + public void setCaptchaAlgorithm(CaptchaAlgorithm captchaAlgorithm) { + this.captchaAlgorithm = captchaAlgorithm; + } + + public CaptchaRepository getCaptchaRepository() { + return captchaRepository; + } + + public void setCaptchaRepository(CaptchaRepository captchaRepository) { + this.captchaRepository = captchaRepository; + } + + /** + *

This method will generate a captcha, returning all of the details of the captcha. Note that the captcha is + * stored so that it can be validated within some time-frame.

+ * @return + */ + + public Captcha generate() { + Captcha captcha = captchaAlgorithm.generate(); + captchaRepository.store(captcha.getToken(), captcha.getResponse()); + return captcha; + } + + /** + *

This will check that the captcha identified by the supplied token has an expected response that matches the + * response that is supplied to this method. It will return true if this is the case. Note that this method will + * also delete the captcha such that it is not able to be verified again or re-used.

+ */ + + public boolean verify(String token, String response) { + Preconditions.checkNotNull(token); + + // maybe better done less frequently? + captchaRepository.purgeExpired(); + + if(Strings.isNullOrEmpty(response)) { + return false; + } + + String databaseResponse = captchaRepository.get(token); + + if(null!=databaseResponse) { + databaseResponse = databaseResponse.trim(); + } + + response.trim(); + + if(null!=databaseResponse) { + captchaRepository.delete(token); + return response.equalsIgnoreCase(databaseResponse); + } + + return false; + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/DatabaseCaptchaRepository.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/DatabaseCaptchaRepository.java new file mode 100644 index 00000000..382cf162 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/DatabaseCaptchaRepository.java @@ -0,0 +1,171 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.captcha; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import org.haikuos.haikudepotserver.captcha.model.CaptchaRepository; +import org.haikuos.haikudepotserver.support.Closeables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Repository; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.sql.DataSource; +import java.sql.*; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + *

This object stores the captchas in a database for later retrieval.

+ */ + +public class DatabaseCaptchaRepository implements CaptchaRepository { + + protected static Logger logger = LoggerFactory.getLogger(DatabaseCaptchaRepository.class); + + private DataSource dataSource; + private Long expirySeconds; + + public DataSource getDataSource() { + return dataSource; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + public Long getExpirySeconds() { + return expirySeconds; + } + + public void setExpirySeconds(Long expirySeconds) { + this.expirySeconds = expirySeconds; + } + + public void init() { + purgeExpired(); + } + + public int purgeExpired() { + Connection connection = null; + PreparedStatement preparedStatement = null; + + try { + connection = dataSource.getConnection(); + preparedStatement = connection.prepareStatement("DELETE FROM captcha.responses WHERE create_timestamp < ?"); + preparedStatement.setTimestamp(1,new Timestamp(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(expirySeconds))); + int deleted = preparedStatement.executeUpdate(); + + if(0!=deleted) { + logger.info("did delete {} expired captcha responses",deleted); + } + + return deleted; + } + catch(SQLException se) { + throw new RuntimeException("unable to purge expired captcha tokens",se); + } + finally { + Closeables.closeQuietly(preparedStatement); + Closeables.closeQuietly(connection); + } + } + + @Override + public boolean delete(String token) { + Preconditions.checkState(!Strings.isNullOrEmpty(token)); + + Connection connection = null; + PreparedStatement preparedStatement = null; + + try { + connection = dataSource.getConnection(); + preparedStatement = connection.prepareStatement("DELETE FROM captcha.responses WHERE token=?"); + preparedStatement.setString(1,token); + int d = preparedStatement.executeUpdate(); + logger.info("did delete captcha token {}",token); + return 1==d; + } + catch(SQLException se) { + throw new RuntimeException("unable to delete captcha token",se); + } + finally { + Closeables.closeQuietly(preparedStatement); + Closeables.closeQuietly(connection); + } + } + + @Override + public String get(String token) { + Preconditions.checkState(!Strings.isNullOrEmpty(token)); + + Connection connection = null; + PreparedStatement preparedStatement = null; + ResultSet resultSet = null; + + try { + connection = dataSource.getConnection(); + preparedStatement = connection.prepareStatement("SELECT response FROM captcha.responses WHERE token=? AND create_timestamp > ?"); + preparedStatement.setString(1,token); + preparedStatement.setTimestamp(2,new Timestamp(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(expirySeconds))); + resultSet = preparedStatement.executeQuery(); + + if(!resultSet.next()) { + return null; + } + + String result = resultSet.getString(1); + + if(resultSet.next()) { + throw new IllegalStateException("found more than one captcha for "+token); + } + + return result; + } + catch(SQLException se) { + throw new RuntimeException("unable to verify captcha token",se); + } + finally { + Closeables.closeQuietly(preparedStatement); + Closeables.closeQuietly(connection); + Closeables.closeQuietly(resultSet); + } + } + + @Override + public void store(String token, String response) { + Preconditions.checkState(!Strings.isNullOrEmpty(token)); + Preconditions.checkState(!Strings.isNullOrEmpty(response)); + + Connection connection = null; + PreparedStatement preparedStatement = null; + + try { + connection = dataSource.getConnection(); + preparedStatement = connection.prepareStatement("INSERT INTO captcha.responses (create_timestamp, token, response) VALUES (?,?,?)"); + preparedStatement.setTimestamp(1,new Timestamp(System.currentTimeMillis())); + preparedStatement.setString(2,token); + preparedStatement.setString(3,response); + + if(1!=preparedStatement.executeUpdate()) { + throw new IllegalStateException("unable to store the captcha token "+token); + } + + logger.info("stored captcha token {}",token); + } + catch(SQLException se) { + throw new RuntimeException("unable to delete captcha token",se); + } + finally { + Closeables.closeQuietly(preparedStatement); + Closeables.closeQuietly(connection); + } + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/SimpleMathProblemCaptchaAlgorithm.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/SimpleMathProblemCaptchaAlgorithm.java new file mode 100644 index 00000000..2afaa294 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/SimpleMathProblemCaptchaAlgorithm.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.captcha; + +import org.haikuos.haikudepotserver.captcha.model.Captcha; +import org.haikuos.haikudepotserver.captcha.model.CaptchaAlgorithm; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Random; +import java.util.UUID; + +/** + *

Implementation of {@link org.haikuos.haikudepotserver.captcha.model.CaptchaAlgorithm} that presents the user with + * a fairly trivial maths problem to solve.

+ */ +public class SimpleMathProblemCaptchaAlgorithm implements CaptchaAlgorithm { + + private static final int PADDING_TEXT = 4; + + private static final int WIDTH = 128; + private static final int HEIGHT = 32; + + private Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 14); + + private BufferedImage bufferedImage; + private Graphics bufferedImageGraphics; + private FontMetrics fontMetrics; + private Random random = new Random(System.currentTimeMillis()); + + public SimpleMathProblemCaptchaAlgorithm() { + super(); + + bufferedImage = new BufferedImage( + WIDTH, + HEIGHT, + BufferedImage.TYPE_INT_RGB); + + bufferedImageGraphics = bufferedImage.getGraphics(); + Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 14); + bufferedImageGraphics.setFont(font); + fontMetrics = bufferedImageGraphics.getFontMetrics(font); + } + + @Override + public synchronized Captcha generate() { + + int addend1 = Math.abs(random.nextInt() % 25); + int addend2 = Math.abs(random.nextInt() % 25); + String problem = String.format("%d + %d",addend1,addend2); + String response = Integer.toString(addend1 + addend2); + byte[] pngImageData; + + synchronized(this) { + + // reset the image. + + bufferedImageGraphics.setColor(Color.DARK_GRAY); + bufferedImageGraphics.fillRect(0,0,bufferedImage.getWidth(),bufferedImage.getHeight()); + + // now render a small image using java 2d with this text in it. + + bufferedImageGraphics.setColor(Color.WHITE); + bufferedImageGraphics.drawString( + problem, + (bufferedImage.getWidth() - fontMetrics.stringWidth(problem)) / 2, + (bufferedImage.getHeight() + fontMetrics.getAscent()) / 2); + + // now generate a PNG of it. + + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, "png", byteArrayOutputStream); + pngImageData = byteArrayOutputStream.toByteArray(); + } + catch(IOException ioe) { + throw new IllegalStateException("unable to write png data in memory",ioe); + } + + } + + Captcha captcha = new Captcha(); + captcha.setToken(UUID.randomUUID().toString()); + captcha.setResponse(response); + captcha.setPngImageData(pngImageData); + return captcha; + + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/Captcha.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/Captcha.java new file mode 100644 index 00000000..88541751 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/Captcha.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.captcha.model; + +/** + *

This object models a captcha; which is an image that contains instructions for a human to convert into text to + * confirm that they are more likely to be a human.

+ */ + +public class Captcha { + + /** + *

This string uniquely identifies the captcha. This is generally the mechanism by which the captcha is + * referenced in other API and services.

+ */ + + private String token; + + /** + *

This is the expected response that a human operator should be expected to provide.

+ */ + + private String response; + + /** + *

This is a PNG image that contains the instruction for the user to convert into some text in their + * response.

+ */ + + private byte[] pngImageData; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } + + public byte[] getPngImageData() { + return pngImageData; + } + + public void setPngImageData(byte[] pngImageData) { + this.pngImageData = pngImageData; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(null!=token ? token.toString() : "???"); + result.append(" --> "); + result.append(getResponse()); + result.append(" ("); + result.append(Integer.toString(pngImageData.length)); + result.append("b)"); + return result.toString(); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/CaptchaAlgorithm.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/CaptchaAlgorithm.java new file mode 100644 index 00000000..d514240a --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/CaptchaAlgorithm.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.captcha.model; + +/** + *

This interface defines an object that is able to generate a new captcha.

+ */ + +public interface CaptchaAlgorithm { + + Captcha generate(); + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/CaptchaRepository.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/CaptchaRepository.java new file mode 100644 index 00000000..6e8bb2ad --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/captcha/model/CaptchaRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.captcha.model; + +/** + *

This interface defines a class that is able to store expected responses against captcha tokens.

+ */ + +public interface CaptchaRepository { + + /** + *

This method will remove those captchas that have expired.

+ */ + + public int purgeExpired(); + + /** + *

This method will delete the captcha identified by the UUID supplied.

+ */ + + public boolean delete(String token); + + /** + *

This method will obtain the response for the captcha identified by the UUID. + */ + + public String get(String token); + + /** + *

This method will store a new token in the database.

+ */ + + public void store(String token, String response); + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/EntryPointController.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/EntryPointController.java new file mode 100644 index 00000000..148730ec --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/EntryPointController.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.controller; + +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.SelectQuery; +import org.haikuos.haikudepotserver.model.Architecture; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import javax.annotation.Resource; +import java.util.List; + +/** + *

This controller renders the default HTML entry point into the application. As this is generally a + * single page application, this controller will just render that single page.

+ */ + +@Controller +@RequestMapping("/") +public class EntryPointController { + + @RequestMapping(method = RequestMethod.GET) + public String entryPoint() { + return "entryPoint"; + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/ImportRepositoryDataController.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/ImportRepositoryDataController.java new file mode 100644 index 00000000..d93e326d --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/ImportRepositoryDataController.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.controller; + +import com.google.common.base.Strings; +import com.google.common.net.HttpHeaders; +import com.google.common.net.MediaType; +import org.haikuos.haikudepotserver.services.ImportRepositoryDataService; +import org.haikuos.haikudepotserver.services.model.ImportRepositoryDataJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

This is the HTTP endpoint from which external systems are able to trigger a repository to be scanned for + * new packages by fetching the HPKR file and processing it. The actual logistics in this controller do not use + * typical Spring MVC error handling and so on; this is because fine control is required and this seems to be + * an easy way to achieve that; basically done manually.

+ */ + +@Controller +@RequestMapping("/importrepositorydata") +public class ImportRepositoryDataController { + + protected static Logger logger = LoggerFactory.getLogger(ImportRepositoryDataController.class); + + public final static String KEY_CODE = "code"; + + @Resource + ImportRepositoryDataService importRepositoryDataService; + + @RequestMapping(method = RequestMethod.GET) + public void fetch( + HttpServletResponse response, + @RequestParam(value = KEY_CODE, required = false) String repositoryCode) { + + try { + if(Strings.isNullOrEmpty(repositoryCode)) { + logger.warn("attempt to import repository data service with no repository code supplied"); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()); + response.getWriter().print(String.format("expected '%s' to have been a query argument to this resource\n",KEY_CODE)); + } + else { + importRepositoryDataService.submit(new ImportRepositoryDataJob(repositoryCode)); + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()); + response.getWriter().print(String.format("accepted import repository job for repository %s\n",repositoryCode)); + } + } + catch(Throwable th) { + logger.error("failed to accept import repository job",th); + + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()); + + try { + response.getWriter().print(String.format("failed to accept import repository job for repository %s\n",repositoryCode)); + } + catch(IOException ioe) { + /* ignore */ + } + } + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/WebResourceGroupController.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/WebResourceGroupController.java new file mode 100644 index 00000000..9cc7f9f7 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/controller/WebResourceGroupController.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.controller; + +import com.google.common.base.Strings; +import com.google.common.io.ByteStreams; +import com.google.common.net.HttpHeaders; +import com.google.common.net.MediaType; +import org.haikuos.haikudepotserver.support.web.model.WebResourceGroup; +import org.haikuos.haikudepotserver.support.Closeables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.Resource; +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 javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + *

Resources such as java-script files and a CSS files can be defined as part of the spring context and then + * this controller is able to take those beans and then render them into a single HTTP download. This avoids the + * page from having to download dozens of small javascript files; it can just request them as one HTTP request.

+ */ + +@Controller +@RequestMapping("/webresourcegroup") +public class WebResourceGroupController implements ApplicationContextAware { + + protected static Logger logger = LoggerFactory.getLogger(WebResourceGroupController.class); + + public final static String KEY_CODE = "code"; + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @RequestMapping(value = "/{code}", method = RequestMethod.GET) + public void fetch( + HttpServletResponse response, + @PathVariable(value = KEY_CODE) String code) + throws IOException { + + if(Strings.isNullOrEmpty(code)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()); + response.getWriter().print(String.format("the code is required")); + } + else { + + String beanId = code + WebResourceGroup.SUFFIX_BEANNAME; + WebResourceGroup webResourceGroup = null; + + try { + webResourceGroup = applicationContext.getBean(beanId, WebResourceGroup.class); + } + catch(NoSuchBeanDefinitionException nsbde) { + // this is ignored because the fact that the webResourceGroup is not defined will + // result in a 404 anyway. + } + + if(null==webResourceGroup || null==webResourceGroup.getResources() || 0==webResourceGroup.getResources().size()) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString()); + response.getWriter().print(String.format("unable to find script group; %s",code)); + } + else { + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader(HttpHeaders.CONTENT_TYPE, webResourceGroup.getMimeType()); + OutputStream outputStream = null; + + try { + + outputStream = response.getOutputStream(); + + for(String resource : webResourceGroup.getResources()) { + Resource r = applicationContext.getResource(resource); + InputStream inputStream = null; + + try { + inputStream = r.getInputStream(); + + if(null==inputStream) { + logger.error("unable to find the resource {} in the script group {}",resource,code); + } + else { + outputStream.write(new byte[] { 0x0d, 0x0d }); + ByteStreams.copy(inputStream, outputStream); + } + } + finally { + Closeables.closeQuietly(inputStream); + } + } + + } + finally { + Closeables.closeQuietly(outputStream); + } + } + } + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Architecture.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Architecture.java new file mode 100644 index 00000000..bfa2deeb --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Architecture.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.query.SortOrder; +import org.haikuos.haikudepotserver.model.auto._Architecture; + +import java.util.List; + +public class Architecture extends _Architecture { + + public final static String CODE_SOURCE = "source"; + public final static String CODE_ANY = "any"; + + public static List getAll(ObjectContext context) { + SelectQuery query = new SelectQuery(Architecture.class); + query.addOrdering(Architecture.CODE_PROPERTY, SortOrder.ASCENDING); + return (List) context.performQuery(query); + } + + public static Optional getByCode(ObjectContext context, String code) { + Preconditions.checkNotNull(context); + Preconditions.checkState(!Strings.isNullOrEmpty(code)); + return Optional.fromNullable(Iterables.getOnlyElement( + (List) context.performQuery(new SelectQuery( + Architecture.class, + ExpressionFactory.matchExp(Architecture.CODE_PROPERTY, code))), + null)); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/HaikuDepot.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/HaikuDepot.java new file mode 100644 index 00000000..2c4e3116 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/HaikuDepot.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import org.haikuos.haikudepotserver.model.auto._HaikuDepot; + +public class HaikuDepot extends _HaikuDepot { + + private static HaikuDepot instance; + + private HaikuDepot() {} + + public static HaikuDepot getInstance() { + if(instance == null) { + instance = new HaikuDepot(); + } + + return instance; + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Pkg.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Pkg.java new file mode 100644 index 00000000..492e6e65 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Pkg.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import com.google.common.base.Optional; +import com.google.common.collect.Iterables; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.validation.SimpleValidationFailure; +import org.apache.cayenne.validation.ValidationResult; +import org.haikuos.haikudepotserver.model.auto._Pkg; +import org.haikuos.haikudepotserver.model.support.CreateAndModifyTimestamped; + +import java.util.List; +import java.util.regex.Pattern; + +public class Pkg extends _Pkg implements CreateAndModifyTimestamped { + + public static Pattern NAME_PATTERN = Pattern.compile("^[^\\s/=!<>-]+$"); + + public static Optional getByName(ObjectContext context, String name) { + return Optional.fromNullable(Iterables.getOnlyElement( + (List) context.performQuery(new SelectQuery( + Pkg.class, + ExpressionFactory.matchExp(Pkg.NAME_PROPERTY, name))), + null)); + } + + @Override + protected void validateForSave(ValidationResult validationResult) { + super.validateForSave(validationResult); + + if(null != getName()) { + if(!NAME_PATTERN.matcher(getName()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,NAME_PATTERN + ".malformed")); + } + } + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgUrlType.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgUrlType.java new file mode 100644 index 00000000..d5f82281 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgUrlType.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import com.google.common.base.Optional; +import com.google.common.collect.Iterables; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.SelectQuery; +import org.haikuos.haikudepotserver.model.auto._PkgUrlType; +import org.haikuos.haikudepotserver.model.support.Coded; + +import java.util.List; + +public class PkgUrlType extends _PkgUrlType implements Coded { + + public static Optional getByCode(ObjectContext context, String code) { + return Optional.fromNullable(Iterables.getOnlyElement( + (List) context.performQuery(new SelectQuery( + PkgUrlType.class, + ExpressionFactory.matchExp(Architecture.CODE_PROPERTY, code))), + null)); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersion.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersion.java new file mode 100644 index 00000000..94eb0809 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersion.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.Ordering; +import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.query.SortOrder; +import org.apache.cayenne.validation.SimpleValidationFailure; +import org.apache.cayenne.validation.ValidationResult; +import org.haikuos.haikudepotserver.model.auto._PkgVersion; +import org.haikuos.haikudepotserver.model.support.CreateAndModifyTimestamped; + +import java.util.List; +import java.util.regex.Pattern; + +public class PkgVersion extends _PkgVersion implements CreateAndModifyTimestamped { + + public final static Pattern MAJOR_PATTERN = Pattern.compile("^[\\w_]+$"); + public final static Pattern MINOR_PATTERN = Pattern.compile("^[\\w_]+$"); + public final static Pattern MICRO_PATTERN = Pattern.compile("^[\\w_.]+$"); + public final static Pattern PRE_RELEASE_PATTERN = Pattern.compile("^[\\w_.]+$"); + + public static List versionOrdering() { + List result = Lists.newArrayList(); + result.add(new Ordering(PkgVersion.MAJOR_PROPERTY, SortOrder.DESCENDING_INSENSITIVE)); + result.add(new Ordering(PkgVersion.MINOR_PROPERTY, SortOrder.DESCENDING_INSENSITIVE)); + result.add(new Ordering(PkgVersion.MICRO_PROPERTY, SortOrder.DESCENDING_INSENSITIVE)); + result.add(new Ordering(PkgVersion.PRE_RELEASE_PROPERTY, SortOrder.DESCENDING_INSENSITIVE)); + result.add(new Ordering(PkgVersion.REVISION_PROPERTY, SortOrder.DESCENDING_INSENSITIVE)); + return result; + } + + public static Optional getLatestForPkg( + ObjectContext context, + Pkg pkg, + List architectures) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(pkg); + Preconditions.checkNotNull(architectures); + Preconditions.checkState(!architectures.isEmpty()); + + Expression architectureExpression = null; + + for(Architecture architecture : architectures) { + if(null==architectureExpression) { + architectureExpression = ExpressionFactory.matchExp(PkgVersion.ARCHITECTURE_PROPERTY, architecture); + } + else { + architectureExpression = architectureExpression.orExp( + ExpressionFactory.matchExp(PkgVersion.ARCHITECTURE_PROPERTY, architecture)); + } + } + + SelectQuery query = new SelectQuery( + PkgVersion.class, + ExpressionFactory.matchExp(PkgVersion.PKG_PROPERTY, pkg).andExp( + ExpressionFactory.matchExp(PkgVersion.ACTIVE_PROPERTY, Boolean.TRUE)).andExp( + architectureExpression)); + + return Optional.fromNullable(Iterables.getOnlyElement( + (List) context.performQuery(query), + null)); + } + + @Override + protected void validateForSave(ValidationResult validationResult) { + super.validateForSave(validationResult); + + if(null != getMajor()) { + if(!MAJOR_PATTERN.matcher(getMajor()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,MAJOR_PROPERTY + ".malformed")); + } + } + + if(null != getMinor()) { + if(!MINOR_PATTERN.matcher(getMinor()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,MINOR_PROPERTY + ".malformed")); + } + } + + if(null != getMicro()) { + if(!MICRO_PATTERN.matcher(getMicro()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,MICRO_PROPERTY + ".malformed")); + } + } + + if(null != getPreRelease()) { + if(!PRE_RELEASE_PATTERN.matcher(getPreRelease()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,PRE_RELEASE_PROPERTY + ".malformed")); + } + } + + if(null != getRevision()) { + if(getRevision().intValue() <= 0) { + validationResult.addFailure(new SimpleValidationFailure(this,REVISION_PROPERTY + ".lessThanEqualZero")); + } + } + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionCopyright.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionCopyright.java new file mode 100644 index 00000000..da50b730 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionCopyright.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import org.haikuos.haikudepotserver.model.auto._PkgVersionCopyright; + +public class PkgVersionCopyright extends _PkgVersionCopyright { + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionLicense.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionLicense.java new file mode 100644 index 00000000..1da858f5 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionLicense.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import org.haikuos.haikudepotserver.model.auto._PkgVersionLicense; + +public class PkgVersionLicense extends _PkgVersionLicense { + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionLocalization.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionLocalization.java new file mode 100644 index 00000000..b62bcf8e --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionLocalization.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import org.haikuos.haikudepotserver.model.auto._PkgVersionLocalization; + +public class PkgVersionLocalization extends _PkgVersionLocalization { + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionUrl.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionUrl.java new file mode 100644 index 00000000..9db16d0a --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/PkgVersionUrl.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import org.apache.cayenne.validation.SimpleValidationFailure; +import org.apache.cayenne.validation.ValidationResult; +import org.haikuos.haikudepotserver.model.auto._PkgVersionUrl; + +import java.net.MalformedURLException; +import java.net.URL; + +public class PkgVersionUrl extends _PkgVersionUrl { + + @Override + protected void validateForSave(ValidationResult validationResult) { + super.validateForSave(validationResult); + + if(null != getUrl()) { + try { + new URL(getUrl()); + } + catch(MalformedURLException mue) { + validationResult.addFailure(new SimpleValidationFailure(this,URL_PROPERTY + ".malformed")); + } + } + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Publisher.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Publisher.java new file mode 100644 index 00000000..6c9666b6 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Publisher.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import org.haikuos.haikudepotserver.model.auto._Publisher; +import org.haikuos.haikudepotserver.model.support.Coded; +import org.haikuos.haikudepotserver.model.support.CreateAndModifyTimestamped; + +public class Publisher extends _Publisher implements CreateAndModifyTimestamped, Coded { + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Repository.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Repository.java new file mode 100644 index 00000000..bfa5591d --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/Repository.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import com.google.common.base.Optional; +import com.google.common.collect.Iterables; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.ObjectIdQuery; +import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.validation.SimpleValidationFailure; +import org.apache.cayenne.validation.ValidationResult; +import org.haikuos.haikudepotserver.model.auto._Repository; +import org.haikuos.haikudepotserver.model.support.Coded; +import org.haikuos.haikudepotserver.model.support.CreateAndModifyTimestamped; + +import java.util.List; + +public class Repository extends _Repository implements CreateAndModifyTimestamped, Coded { + + public static Repository get(ObjectContext context, ObjectId objectId) { + return Iterables.getOnlyElement((List) context.performQuery(new ObjectIdQuery(objectId))); + } + + public static Optional getByCode(ObjectContext context, String code) { + return Optional.fromNullable(Iterables.getOnlyElement( + (List) context.performQuery(new SelectQuery( + Repository.class, + ExpressionFactory.matchExp(Repository.CODE_PROPERTY, code))), + null)); + } + + @Override + protected void validateForSave(ValidationResult validationResult) { + super.validateForSave(validationResult); + + if(null != getCode()) { + if(!CODE_PATTERN.matcher(getCode()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,CODE_PROPERTY + ".malformed")); + } + } + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/User.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/User.java new file mode 100644 index 00000000..62aab493 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/User.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.hash.Hashing; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.validation.SimpleValidationFailure; +import org.apache.cayenne.validation.ValidationFailure; +import org.apache.cayenne.validation.ValidationResult; +import org.haikuos.haikudepotserver.model.auto._User; + +import java.util.List; +import java.util.UUID; +import java.util.regex.Pattern; + +public class User extends _User { + + public final static Pattern NICKNAME_PATTERN = Pattern.compile("^[\\w]{4,16}$"); + public final static Pattern PASSWORDHASH_PATTERN = Pattern.compile("^[a-f0-9]{64}$"); + public final static Pattern PASSWORDSALT_PATTERN = Pattern.compile("^[a-f0-9]{64}$"); + + public static Optional getByNickname(ObjectContext context, String nickname) { + return Optional.fromNullable(Iterables.getOnlyElement( + (List) context.performQuery(new SelectQuery( + User.class, + ExpressionFactory.matchExp(User.NICKNAME_PROPERTY, nickname))), + null)); + } + + // configured as a listener method in the model. + + public void onPostAdd() { + + if(null==getIsRoot()) { + setIsRoot(Boolean.FALSE); + } + + if(null==getCanManageUsers()) { + setCanManageUsers(Boolean.FALSE); + } + + if(null==getActive()) { + setActive(Boolean.TRUE); + } + + if(null==getPasswordSalt()) { + setPasswordSalt(UUID.randomUUID().toString()); + } + + } + + @Override + protected void validateForSave(ValidationResult validationResult) { + super.validateForSave(validationResult); + + if(null==getIsRoot()) { + setIsRoot(Boolean.FALSE); + } + + if(null != getNickname()) { + if(!NICKNAME_PATTERN.matcher(getNickname()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,NICKNAME_PROPERTY + ".malformed")); + } + } + + if(null != getPasswordHash()) { + if(!PASSWORDHASH_PATTERN.matcher(getPasswordHash()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,PASSWORD_HASH_PROPERTY + ".malformed")); + } + } + + if(null != getPasswordSalt()) { + if(!PASSWORDSALT_PATTERN.matcher(getPasswordSalt()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,PASSWORD_HASH_PROPERTY + ".malformed")); + } + } + + } + + /** + *

This method will configure a random salt value.

+ */ + + public void setPasswordSalt() { + setPasswordSalt(Hashing.sha256().hashString(UUID.randomUUID().toString()).toString()); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Architecture.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Architecture.java new file mode 100644 index 00000000..295655b0 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Architecture.java @@ -0,0 +1,21 @@ +package org.haikuos.haikudepotserver.model.auto; + +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _Architecture was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Architecture extends AbstractDataObject { + + public static final String CODE_PROPERTY = "code"; + + public static final String ID_PK_COLUMN = "id"; + + public String getCode() { + return (String)readProperty(CODE_PROPERTY); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_HaikuDepot.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_HaikuDepot.java new file mode 100644 index 00000000..ab91c690 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_HaikuDepot.java @@ -0,0 +1,12 @@ +package org.haikuos.haikudepotserver.model.auto; + + + +/** + * This class was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public class _HaikuDepot { +} \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Pkg.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Pkg.java new file mode 100644 index 00000000..8fcabeac --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Pkg.java @@ -0,0 +1,61 @@ +package org.haikuos.haikudepotserver.model.auto; + +import java.util.Date; + +import org.haikuos.haikudepotserver.model.Publisher; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _Pkg was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Pkg extends AbstractDataObject { + + public static final String ACTIVE_PROPERTY = "active"; + public static final String CREATE_TIMESTAMP_PROPERTY = "createTimestamp"; + public static final String MODIFY_TIMESTAMP_PROPERTY = "modifyTimestamp"; + public static final String NAME_PROPERTY = "name"; + public static final String PUBLISHER_PROPERTY = "publisher"; + + public static final String ID_PK_COLUMN = "id"; + + public void setActive(Boolean active) { + writeProperty(ACTIVE_PROPERTY, active); + } + public Boolean getActive() { + return (Boolean)readProperty(ACTIVE_PROPERTY); + } + + public void setCreateTimestamp(Date createTimestamp) { + writeProperty(CREATE_TIMESTAMP_PROPERTY, createTimestamp); + } + public Date getCreateTimestamp() { + return (Date)readProperty(CREATE_TIMESTAMP_PROPERTY); + } + + public void setModifyTimestamp(Date modifyTimestamp) { + writeProperty(MODIFY_TIMESTAMP_PROPERTY, modifyTimestamp); + } + public Date getModifyTimestamp() { + return (Date)readProperty(MODIFY_TIMESTAMP_PROPERTY); + } + + public void setName(String name) { + writeProperty(NAME_PROPERTY, name); + } + public String getName() { + return (String)readProperty(NAME_PROPERTY); + } + + public void setPublisher(Publisher publisher) { + setToOneTarget(PUBLISHER_PROPERTY, publisher, true); + } + + public Publisher getPublisher() { + return (Publisher)readProperty(PUBLISHER_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgUrlType.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgUrlType.java new file mode 100644 index 00000000..0f8be9ff --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgUrlType.java @@ -0,0 +1,21 @@ +package org.haikuos.haikudepotserver.model.auto; + +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _PkgUrlType was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _PkgUrlType extends AbstractDataObject { + + public static final String CODE_PROPERTY = "code"; + + public static final String ID_PK_COLUMN = "id"; + + public String getCode() { + return (String)readProperty(CODE_PROPERTY); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersion.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersion.java new file mode 100644 index 00000000..56af8b2e --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersion.java @@ -0,0 +1,172 @@ +package org.haikuos.haikudepotserver.model.auto; + +import java.util.Date; +import java.util.List; + +import org.haikuos.haikudepotserver.model.Architecture; +import org.haikuos.haikudepotserver.model.Pkg; +import org.haikuos.haikudepotserver.model.PkgVersionCopyright; +import org.haikuos.haikudepotserver.model.PkgVersionLicense; +import org.haikuos.haikudepotserver.model.PkgVersionLocalization; +import org.haikuos.haikudepotserver.model.PkgVersionUrl; +import org.haikuos.haikudepotserver.model.Repository; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _PkgVersion was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _PkgVersion extends AbstractDataObject { + + public static final String ACTIVE_PROPERTY = "active"; + public static final String CREATE_TIMESTAMP_PROPERTY = "createTimestamp"; + public static final String MAJOR_PROPERTY = "major"; + public static final String MICRO_PROPERTY = "micro"; + public static final String MINOR_PROPERTY = "minor"; + public static final String MODIFY_TIMESTAMP_PROPERTY = "modifyTimestamp"; + public static final String PRE_RELEASE_PROPERTY = "preRelease"; + public static final String REVISION_PROPERTY = "revision"; + public static final String ARCHITECTURE_PROPERTY = "architecture"; + public static final String PKG_PROPERTY = "pkg"; + public static final String PKG_VERSION_COPYRIGHTS_PROPERTY = "pkgVersionCopyrights"; + public static final String PKG_VERSION_LICENSES_PROPERTY = "pkgVersionLicenses"; + public static final String PKG_VERSION_LOCALIZATIONS_PROPERTY = "pkgVersionLocalizations"; + public static final String PKG_VERSION_URLS_PROPERTY = "pkgVersionUrls"; + public static final String REPOSITORY_PROPERTY = "repository"; + + public static final String ID_PK_COLUMN = "id"; + + public void setActive(Boolean active) { + writeProperty(ACTIVE_PROPERTY, active); + } + public Boolean getActive() { + return (Boolean)readProperty(ACTIVE_PROPERTY); + } + + public void setCreateTimestamp(Date createTimestamp) { + writeProperty(CREATE_TIMESTAMP_PROPERTY, createTimestamp); + } + public Date getCreateTimestamp() { + return (Date)readProperty(CREATE_TIMESTAMP_PROPERTY); + } + + public void setMajor(String major) { + writeProperty(MAJOR_PROPERTY, major); + } + public String getMajor() { + return (String)readProperty(MAJOR_PROPERTY); + } + + public void setMicro(String micro) { + writeProperty(MICRO_PROPERTY, micro); + } + public String getMicro() { + return (String)readProperty(MICRO_PROPERTY); + } + + public void setMinor(String minor) { + writeProperty(MINOR_PROPERTY, minor); + } + public String getMinor() { + return (String)readProperty(MINOR_PROPERTY); + } + + public void setModifyTimestamp(Date modifyTimestamp) { + writeProperty(MODIFY_TIMESTAMP_PROPERTY, modifyTimestamp); + } + public Date getModifyTimestamp() { + return (Date)readProperty(MODIFY_TIMESTAMP_PROPERTY); + } + + public void setPreRelease(String preRelease) { + writeProperty(PRE_RELEASE_PROPERTY, preRelease); + } + public String getPreRelease() { + return (String)readProperty(PRE_RELEASE_PROPERTY); + } + + public void setRevision(Integer revision) { + writeProperty(REVISION_PROPERTY, revision); + } + public Integer getRevision() { + return (Integer)readProperty(REVISION_PROPERTY); + } + + public void setArchitecture(Architecture architecture) { + setToOneTarget(ARCHITECTURE_PROPERTY, architecture, true); + } + + public Architecture getArchitecture() { + return (Architecture)readProperty(ARCHITECTURE_PROPERTY); + } + + + public void setPkg(Pkg pkg) { + setToOneTarget(PKG_PROPERTY, pkg, true); + } + + public Pkg getPkg() { + return (Pkg)readProperty(PKG_PROPERTY); + } + + + public void addToPkgVersionCopyrights(PkgVersionCopyright obj) { + addToManyTarget(PKG_VERSION_COPYRIGHTS_PROPERTY, obj, true); + } + public void removeFromPkgVersionCopyrights(PkgVersionCopyright obj) { + removeToManyTarget(PKG_VERSION_COPYRIGHTS_PROPERTY, obj, true); + } + @SuppressWarnings("unchecked") + public List getPkgVersionCopyrights() { + return (List)readProperty(PKG_VERSION_COPYRIGHTS_PROPERTY); + } + + + public void addToPkgVersionLicenses(PkgVersionLicense obj) { + addToManyTarget(PKG_VERSION_LICENSES_PROPERTY, obj, true); + } + public void removeFromPkgVersionLicenses(PkgVersionLicense obj) { + removeToManyTarget(PKG_VERSION_LICENSES_PROPERTY, obj, true); + } + @SuppressWarnings("unchecked") + public List getPkgVersionLicenses() { + return (List)readProperty(PKG_VERSION_LICENSES_PROPERTY); + } + + + public void addToPkgVersionLocalizations(PkgVersionLocalization obj) { + addToManyTarget(PKG_VERSION_LOCALIZATIONS_PROPERTY, obj, true); + } + public void removeFromPkgVersionLocalizations(PkgVersionLocalization obj) { + removeToManyTarget(PKG_VERSION_LOCALIZATIONS_PROPERTY, obj, true); + } + @SuppressWarnings("unchecked") + public List getPkgVersionLocalizations() { + return (List)readProperty(PKG_VERSION_LOCALIZATIONS_PROPERTY); + } + + + public void addToPkgVersionUrls(PkgVersionUrl obj) { + addToManyTarget(PKG_VERSION_URLS_PROPERTY, obj, true); + } + public void removeFromPkgVersionUrls(PkgVersionUrl obj) { + removeToManyTarget(PKG_VERSION_URLS_PROPERTY, obj, true); + } + @SuppressWarnings("unchecked") + public List getPkgVersionUrls() { + return (List)readProperty(PKG_VERSION_URLS_PROPERTY); + } + + + public void setRepository(Repository repository) { + setToOneTarget(REPOSITORY_PROPERTY, repository, true); + } + + public Repository getRepository() { + return (Repository)readProperty(REPOSITORY_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionCopyright.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionCopyright.java new file mode 100644 index 00000000..b91cfd10 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionCopyright.java @@ -0,0 +1,35 @@ +package org.haikuos.haikudepotserver.model.auto; + +import org.haikuos.haikudepotserver.model.PkgVersion; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _PkgVersionCopyright was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _PkgVersionCopyright extends AbstractDataObject { + + public static final String BODY_PROPERTY = "body"; + public static final String PKG_VERSION_PROPERTY = "pkgVersion"; + + public static final String ID_PK_COLUMN = "id"; + + public void setBody(String body) { + writeProperty(BODY_PROPERTY, body); + } + public String getBody() { + return (String)readProperty(BODY_PROPERTY); + } + + public void setPkgVersion(PkgVersion pkgVersion) { + setToOneTarget(PKG_VERSION_PROPERTY, pkgVersion, true); + } + + public PkgVersion getPkgVersion() { + return (PkgVersion)readProperty(PKG_VERSION_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionLicense.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionLicense.java new file mode 100644 index 00000000..94283eeb --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionLicense.java @@ -0,0 +1,35 @@ +package org.haikuos.haikudepotserver.model.auto; + +import org.haikuos.haikudepotserver.model.PkgVersion; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _PkgVersionLicense was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _PkgVersionLicense extends AbstractDataObject { + + public static final String BODY_PROPERTY = "body"; + public static final String PKG_VERSION_PROPERTY = "pkgVersion"; + + public static final String ID_PK_COLUMN = "id"; + + public void setBody(String body) { + writeProperty(BODY_PROPERTY, body); + } + public String getBody() { + return (String)readProperty(BODY_PROPERTY); + } + + public void setPkgVersion(PkgVersion pkgVersion) { + setToOneTarget(PKG_VERSION_PROPERTY, pkgVersion, true); + } + + public PkgVersion getPkgVersion() { + return (PkgVersion)readProperty(PKG_VERSION_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionLocalization.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionLocalization.java new file mode 100644 index 00000000..80d63dc7 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionLocalization.java @@ -0,0 +1,43 @@ +package org.haikuos.haikudepotserver.model.auto; + +import org.haikuos.haikudepotserver.model.PkgVersion; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _PkgVersionLocalization was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _PkgVersionLocalization extends AbstractDataObject { + + public static final String DESCRIPTION_PROPERTY = "description"; + public static final String SUMMARY_PROPERTY = "summary"; + public static final String PKG_VERSION_PROPERTY = "pkgVersion"; + + public static final String ID_PK_COLUMN = "id"; + + public void setDescription(String description) { + writeProperty(DESCRIPTION_PROPERTY, description); + } + public String getDescription() { + return (String)readProperty(DESCRIPTION_PROPERTY); + } + + public void setSummary(String summary) { + writeProperty(SUMMARY_PROPERTY, summary); + } + public String getSummary() { + return (String)readProperty(SUMMARY_PROPERTY); + } + + public void setPkgVersion(PkgVersion pkgVersion) { + setToOneTarget(PKG_VERSION_PROPERTY, pkgVersion, true); + } + + public PkgVersion getPkgVersion() { + return (PkgVersion)readProperty(PKG_VERSION_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionUrl.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionUrl.java new file mode 100644 index 00000000..6be8b3ab --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_PkgVersionUrl.java @@ -0,0 +1,46 @@ +package org.haikuos.haikudepotserver.model.auto; + +import org.haikuos.haikudepotserver.model.PkgUrlType; +import org.haikuos.haikudepotserver.model.PkgVersion; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _PkgVersionUrl was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _PkgVersionUrl extends AbstractDataObject { + + public static final String URL_PROPERTY = "url"; + public static final String PKG_URL_TYPE_PROPERTY = "pkgUrlType"; + public static final String PKG_VERSION_PROPERTY = "pkgVersion"; + + public static final String ID_PK_COLUMN = "id"; + + public void setUrl(String url) { + writeProperty(URL_PROPERTY, url); + } + public String getUrl() { + return (String)readProperty(URL_PROPERTY); + } + + public void setPkgUrlType(PkgUrlType pkgUrlType) { + setToOneTarget(PKG_URL_TYPE_PROPERTY, pkgUrlType, true); + } + + public PkgUrlType getPkgUrlType() { + return (PkgUrlType)readProperty(PKG_URL_TYPE_PROPERTY); + } + + + public void setPkgVersion(PkgVersion pkgVersion) { + setToOneTarget(PKG_VERSION_PROPERTY, pkgVersion, true); + } + + public PkgVersion getPkgVersion() { + return (PkgVersion)readProperty(PKG_VERSION_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Publisher.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Publisher.java new file mode 100644 index 00000000..c1dff0d1 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Publisher.java @@ -0,0 +1,89 @@ +package org.haikuos.haikudepotserver.model.auto; + +import java.util.Date; +import java.util.List; + +import org.haikuos.haikudepotserver.model.Pkg; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _Publisher was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Publisher extends AbstractDataObject { + + public static final String ACTIVE_PROPERTY = "active"; + public static final String CODE_PROPERTY = "code"; + public static final String CREATE_TIMESTAMP_PROPERTY = "createTimestamp"; + public static final String EMAIL_PROPERTY = "email"; + public static final String MODIFY_TIMESTAMP_PROPERTY = "modifyTimestamp"; + public static final String NAME_PROPERTY = "name"; + public static final String SITE_URL_PROPERTY = "siteUrl"; + public static final String PKGS_PROPERTY = "pkgs"; + + public static final String ID_PK_COLUMN = "id"; + + public void setActive(Boolean active) { + writeProperty(ACTIVE_PROPERTY, active); + } + public Boolean getActive() { + return (Boolean)readProperty(ACTIVE_PROPERTY); + } + + public void setCode(String code) { + writeProperty(CODE_PROPERTY, code); + } + public String getCode() { + return (String)readProperty(CODE_PROPERTY); + } + + public void setCreateTimestamp(Date createTimestamp) { + writeProperty(CREATE_TIMESTAMP_PROPERTY, createTimestamp); + } + public Date getCreateTimestamp() { + return (Date)readProperty(CREATE_TIMESTAMP_PROPERTY); + } + + public void setEmail(String email) { + writeProperty(EMAIL_PROPERTY, email); + } + public String getEmail() { + return (String)readProperty(EMAIL_PROPERTY); + } + + public void setModifyTimestamp(Date modifyTimestamp) { + writeProperty(MODIFY_TIMESTAMP_PROPERTY, modifyTimestamp); + } + public Date getModifyTimestamp() { + return (Date)readProperty(MODIFY_TIMESTAMP_PROPERTY); + } + + public void setName(String name) { + writeProperty(NAME_PROPERTY, name); + } + public String getName() { + return (String)readProperty(NAME_PROPERTY); + } + + public void setSiteUrl(String siteUrl) { + writeProperty(SITE_URL_PROPERTY, siteUrl); + } + public String getSiteUrl() { + return (String)readProperty(SITE_URL_PROPERTY); + } + + public void addToPkgs(Pkg obj) { + addToManyTarget(PKGS_PROPERTY, obj, true); + } + public void removeFromPkgs(Pkg obj) { + removeToManyTarget(PKGS_PROPERTY, obj, true); + } + @SuppressWarnings("unchecked") + public List getPkgs() { + return (List)readProperty(PKGS_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Repository.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Repository.java new file mode 100644 index 00000000..a66f23ea --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_Repository.java @@ -0,0 +1,69 @@ +package org.haikuos.haikudepotserver.model.auto; + +import java.util.Date; + +import org.haikuos.haikudepotserver.model.Architecture; +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _Repository was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Repository extends AbstractDataObject { + + public static final String ACTIVE_PROPERTY = "active"; + public static final String CODE_PROPERTY = "code"; + public static final String CREATE_TIMESTAMP_PROPERTY = "createTimestamp"; + public static final String MODIFY_TIMESTAMP_PROPERTY = "modifyTimestamp"; + public static final String URL_PROPERTY = "url"; + public static final String ARCHITECTURE_PROPERTY = "architecture"; + + public static final String ID_PK_COLUMN = "id"; + + public void setActive(Boolean active) { + writeProperty(ACTIVE_PROPERTY, active); + } + public Boolean getActive() { + return (Boolean)readProperty(ACTIVE_PROPERTY); + } + + public void setCode(String code) { + writeProperty(CODE_PROPERTY, code); + } + public String getCode() { + return (String)readProperty(CODE_PROPERTY); + } + + public void setCreateTimestamp(Date createTimestamp) { + writeProperty(CREATE_TIMESTAMP_PROPERTY, createTimestamp); + } + public Date getCreateTimestamp() { + return (Date)readProperty(CREATE_TIMESTAMP_PROPERTY); + } + + public void setModifyTimestamp(Date modifyTimestamp) { + writeProperty(MODIFY_TIMESTAMP_PROPERTY, modifyTimestamp); + } + public Date getModifyTimestamp() { + return (Date)readProperty(MODIFY_TIMESTAMP_PROPERTY); + } + + public void setUrl(String url) { + writeProperty(URL_PROPERTY, url); + } + public String getUrl() { + return (String)readProperty(URL_PROPERTY); + } + + public void setArchitecture(Architecture architecture) { + setToOneTarget(ARCHITECTURE_PROPERTY, architecture, true); + } + + public Architecture getArchitecture() { + return (Architecture)readProperty(ARCHITECTURE_PROPERTY); + } + + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_User.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_User.java new file mode 100644 index 00000000..d1e13148 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/auto/_User.java @@ -0,0 +1,64 @@ +package org.haikuos.haikudepotserver.model.auto; + +import org.haikuos.haikudepotserver.model.support.AbstractDataObject; + +/** + * Class _User was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _User extends AbstractDataObject { + + public static final String ACTIVE_PROPERTY = "active"; + public static final String CAN_MANAGE_USERS_PROPERTY = "canManageUsers"; + public static final String IS_ROOT_PROPERTY = "isRoot"; + public static final String NICKNAME_PROPERTY = "nickname"; + public static final String PASSWORD_HASH_PROPERTY = "passwordHash"; + public static final String PASSWORD_SALT_PROPERTY = "passwordSalt"; + + public static final String ID_PK_COLUMN = "id"; + + public void setActive(Boolean active) { + writeProperty(ACTIVE_PROPERTY, active); + } + public Boolean getActive() { + return (Boolean)readProperty(ACTIVE_PROPERTY); + } + + public void setCanManageUsers(Boolean canManageUsers) { + writeProperty(CAN_MANAGE_USERS_PROPERTY, canManageUsers); + } + public Boolean getCanManageUsers() { + return (Boolean)readProperty(CAN_MANAGE_USERS_PROPERTY); + } + + public void setIsRoot(Boolean isRoot) { + writeProperty(IS_ROOT_PROPERTY, isRoot); + } + public Boolean getIsRoot() { + return (Boolean)readProperty(IS_ROOT_PROPERTY); + } + + public void setNickname(String nickname) { + writeProperty(NICKNAME_PROPERTY, nickname); + } + public String getNickname() { + return (String)readProperty(NICKNAME_PROPERTY); + } + + public void setPasswordHash(String passwordHash) { + writeProperty(PASSWORD_HASH_PROPERTY, passwordHash); + } + public String getPasswordHash() { + return (String)readProperty(PASSWORD_HASH_PROPERTY); + } + + public void setPasswordSalt(String passwordSalt) { + writeProperty(PASSWORD_SALT_PROPERTY, passwordSalt); + } + public String getPasswordSalt() { + return (String)readProperty(PASSWORD_SALT_PROPERTY); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/package-info.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/package-info.java new file mode 100644 index 00000000..92d08d6d --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/package-info.java @@ -0,0 +1,8 @@ +/** + *

These model objects are initially created by the Cayenne modeller application using the 'gap pattern'. The + * objects in this package are generally able to be edited to augment them with validation and so on. The "auto" + * sub-package contains automatically re-generated files that should not be altered because the modeller may be used + * to re-generate those base classes.

+ */ + +package org.haikuos.haikudepotserver.model; \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/AbstractDataObject.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/AbstractDataObject.java new file mode 100644 index 00000000..66f50761 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/AbstractDataObject.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model.support; + +import org.apache.cayenne.CayenneDataObject; +import org.apache.cayenne.validation.SimpleValidationFailure; +import org.apache.cayenne.validation.ValidationResult; + +import java.util.regex.Pattern; + +/** + *

This is the superclass of the Cayanne Data Objects in this project. This contains some common handling for + * all data objects.

+ */ + +public abstract class AbstractDataObject extends CayenneDataObject { + + public final static Pattern CODE_PATTERN = Pattern.compile("^[a-f0-9]{2,16}$"); + + @Override + protected void validateForSave(ValidationResult validationResult) { + super.validateForSave(validationResult); + + // If we implement the Coded interface then we can validate the code on this + // object to check it is valid. + + if(Coded.class.isAssignableFrom(this.getClass())) { + + Coded coded = (Coded) this; + + if(null != coded.getCode()) { + if(!CODE_PATTERN.matcher(coded.getCode()).matches()) { + validationResult.addFailure(new SimpleValidationFailure(this,"code.malformed")); + } + } + } + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/Coded.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/Coded.java new file mode 100644 index 00000000..2fe157f9 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/Coded.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model.support; + +/** + *

This interface defines a method to get a code. This is used mostly on reference data such as + * {@link org.haikuos.haikudepotserver.model.PkgUrlType} for example where the code provides a + * machine-reference (not human readable) identification mechanism for an instance of an object.

+ */ + +public interface Coded { + + String getCode(); + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/CreateAndModifyTimestamped.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/CreateAndModifyTimestamped.java new file mode 100644 index 00000000..d2c2cd86 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/model/support/CreateAndModifyTimestamped.java @@ -0,0 +1,23 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.model.support; + +import java.util.Date; + +/** + *

This is an interface for objects that are capable of storing and providing a create and modify timestamp. A + * listener is then able to operate on such objects observing changes and thereby updating the modify timestamp on + * the instance of the object in question. This avoids the need to manually maintain these modify timestamps.

+ */ + +public interface CreateAndModifyTimestamped { + + public void setCreateTimestamp(Date createTimestamp); + public Date getCreateTimestamp(); + public void setModifyTimestamp(Date modifyTimestamp); + public Date getModifyTimestamp(); + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/ImportPackageService.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/ImportPackageService.java new file mode 100644 index 00000000..4353b6b8 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/ImportPackageService.java @@ -0,0 +1,157 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.services; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.SelectQuery; +import org.haikuos.haikudepotserver.model.*; +import org.haikuos.pkg.model.Pkg; +import org.haikuos.pkg.model.PkgVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + *

This class will import a package into the system's database; merging the data, or disabling entries as + * necessary.

+ */ + +@Service +public class ImportPackageService { + + protected static Logger logger = LoggerFactory.getLogger(ImportPackageService.class); + + private Expression toExpression(PkgVersion version) { + return ExpressionFactory.matchExp( + org.haikuos.haikudepotserver.model.PkgVersion.MAJOR_PROPERTY, version.getMajor()) + .andExp(ExpressionFactory.matchExp( + org.haikuos.haikudepotserver.model.PkgVersion.MINOR_PROPERTY, version.getMinor())) + .andExp(ExpressionFactory.matchExp( + org.haikuos.haikudepotserver.model.PkgVersion.MICRO_PROPERTY, version.getMicro())) + .andExp(ExpressionFactory.matchExp( + org.haikuos.haikudepotserver.model.PkgVersion.PRE_RELEASE_PROPERTY, version.getPreRelease())) + .andExp(ExpressionFactory.matchExp( + org.haikuos.haikudepotserver.model.PkgVersion.REVISION_PROPERTY, version.getRevision())); + } + + /** + *

This is the Cayenne environment handle.

+ */ + + @Resource + ServerRuntime serverRuntime; + + /** + *

This method will import the pkg described. The repository is also provided as a Cayenne object id in order + * to provide reference to the repository from which this pkg was obtained. Note that this method will execute + * as one 'transaction' (in the Cayenne sense).

+ */ + + public void run( + ObjectId repositoryObjectId, + Pkg pkg) { + + Preconditions.checkNotNull(pkg); + Preconditions.checkNotNull(repositoryObjectId); + + ObjectContext objectContext = serverRuntime.getContext(); + Repository repository = Repository.get(objectContext, repositoryObjectId); + + // first, check to see if the package is there or not. + + Optional persistedPkgOptional = org.haikuos.haikudepotserver.model.Pkg.getByName(objectContext, pkg.getName()); + org.haikuos.haikudepotserver.model.Pkg persistedPkg; + org.haikuos.haikudepotserver.model.PkgVersion persistedPkgVersion = null; + + if(!persistedPkgOptional.isPresent()) { + persistedPkg = objectContext.newObject(org.haikuos.haikudepotserver.model.Pkg.class); + persistedPkg.setName(pkg.getName()); + persistedPkg.setActive(Boolean.TRUE); + logger.info("the package {} did not exist; will create",pkg.getName()); + } + else { + persistedPkg = persistedPkgOptional.get(); + + // if we know that the package exists then we should look for the version. + + SelectQuery selectQuery = new SelectQuery( + org.haikuos.haikudepotserver.model.PkgVersion.class, + ExpressionFactory.matchExp( + org.haikuos.haikudepotserver.model.PkgVersion.PKG_PROPERTY, + persistedPkg) + .andExp(toExpression(pkg.getVersion()))); + + persistedPkgVersion = Iterables.getOnlyElement( + (List) objectContext.performQuery(selectQuery), + null); + } + + if(null==persistedPkgVersion) { + + persistedPkgVersion = objectContext.newObject(org.haikuos.haikudepotserver.model.PkgVersion.class); + persistedPkgVersion.setActive(Boolean.TRUE); + persistedPkgVersion.setMajor(pkg.getVersion().getMajor()); + persistedPkgVersion.setMinor(pkg.getVersion().getMinor()); + persistedPkgVersion.setMicro(pkg.getVersion().getMicro()); + persistedPkgVersion.setPreRelease(pkg.getVersion().getPreRelease()); + persistedPkgVersion.setRevision(pkg.getVersion().getRevision()); + persistedPkgVersion.setRepository(repository); + persistedPkgVersion.setArchitecture(Architecture.getByCode( + objectContext, + pkg.getArchitecture().name().toLowerCase()).get()); + persistedPkgVersion.setPkg(persistedPkg); + + // now add the copyrights + for(String copyright : pkg.getCopyrights()) { + PkgVersionCopyright persistedPkgVersionCopyright = objectContext.newObject(PkgVersionCopyright.class); + persistedPkgVersionCopyright.setBody(copyright); + persistedPkgVersionCopyright.setPkgVersion(persistedPkgVersion); + } + + // now add the licenses + for(String license : pkg.getLicenses()) { + PkgVersionLicense persistedPkgVersionLicense = objectContext.newObject(PkgVersionLicense.class); + persistedPkgVersionLicense.setBody(license); + persistedPkgVersionLicense.setPkgVersion(persistedPkgVersion); + } + + if(null!=pkg.getHomePageUrl()) { + PkgVersionUrl persistedPkgVersionUrl = objectContext.newObject(PkgVersionUrl.class); + persistedPkgVersionUrl.setUrl(pkg.getHomePageUrl().getUrl()); + persistedPkgVersionUrl.setPkgUrlType(PkgUrlType.getByCode( + objectContext, + pkg.getHomePageUrl().getUrlType().name().toLowerCase()).get()); + persistedPkgVersionUrl.setPkgVersion(persistedPkgVersion); + } + + if(!Strings.isNullOrEmpty(pkg.getSummary()) || !Strings.isNullOrEmpty(pkg.getDescription())) { + PkgVersionLocalization persistedPkgVersionLocalization = objectContext.newObject(PkgVersionLocalization.class); + persistedPkgVersionLocalization.setDescription(pkg.getDescription()); + persistedPkgVersionLocalization.setSummary(pkg.getSummary()); + persistedPkgVersionLocalization.setPkgVersion(persistedPkgVersion); + } + + logger.info("the version {} of package {} did not exist; will create", pkg.getVersion().toString(), pkg.getName()); + } + + objectContext.commitChanges(); + + logger.info("have processed package {}",pkg.toString()); + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/ImportRepositoryDataService.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/ImportRepositoryDataService.java new file mode 100644 index 00000000..9112f019 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/ImportRepositoryDataService.java @@ -0,0 +1,194 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.services; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Queues; +import com.google.common.io.InputSupplier; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.haikuos.haikudepotserver.model.Repository; +import org.haikuos.haikudepotserver.services.model.ImportRepositoryDataJob; +import org.haikuos.haikudepotserver.support.Closeables; +import org.haikuos.pkg.PkgIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + *

This object is responsible for migrating a HPKR file from a remote repository into the Haiku Depot Server + * database. It will copy the data into a local file and then work through it there. Note that this does + * not run the entire process in a single transaction; it will execute one transaction per package. So if the + * process fails, a repository update is likely to be partially imported.

+ * + *

The system works by the caller lodging a request to update from a remote repository. The request may be + * later superceeded by another request for the same repository. When the import process has capacity then it + * will undertake the import process.

+ */ + +@Service +public class ImportRepositoryDataService { + + protected static Logger logger = LoggerFactory.getLogger(ImportRepositoryDataService.class); + + public final static int SIZE_QUEUE = 10; + + @Resource + ServerRuntime serverRuntime; + + @Resource + ImportPackageService importPackageService; + + private ThreadPoolExecutor executor = null; + + private ArrayBlockingQueue runnables = Queues.newArrayBlockingQueue(SIZE_QUEUE); + + private ThreadPoolExecutor getExecutor() { + if(null==executor) { + executor = new ThreadPoolExecutor( + 0, // core pool size + 1, // max pool size + 1l, // time to shutdown threads + TimeUnit.MINUTES, + runnables, + new ThreadPoolExecutor.AbortPolicy()); + } + + return executor; + } + + /** + *

This method will check that there is not already a job in the queue for this repository and then will + * add it to the queue so that it is run at some time in the future.

+ * @param job + */ + + public void submit(final ImportRepositoryDataJob job) { + Preconditions.checkNotNull(job); + + // first thing to do is to validate the request; does the repository exist and what is it's URL? + Optional repositoryOptional = Repository.getByCode(serverRuntime.getContext(), job.getCode()); + + if(!repositoryOptional.isPresent()) { + throw new RuntimeException("unable to import repository data because repository was not able to be found for code; "+job.getCode()); + } + + if(!Iterables.tryFind(runnables, new Predicate() { + @Override + public boolean apply(java.lang.Runnable input) { + ImportRepositoryDataJobRunnable importRepositoryDataJobRunnable = (ImportRepositoryDataJobRunnable) input; + return importRepositoryDataJobRunnable.equals(job); + } + }).isPresent()) { + getExecutor().submit(new ImportRepositoryDataJobRunnable(this,job)); + logger.info("have submitted job to import repository data; {}", job.toString()); + } + else { + logger.info("ignoring job to import repository data as there is already one waiting; {}", job.toString()); + + } + } + + protected void run(ImportRepositoryDataJob job) { + Preconditions.checkNotNull(job); + + Repository repository = Repository.getByCode(serverRuntime.getContext(), job.getCode()).get(); + URL url; + + try { + url = new URL(repository.getUrl()); + } + catch(MalformedURLException mue) { + throw new IllegalStateException("the repository "+job.getCode()+" has a malformed url; "+repository.getUrl(),mue); + } + + // now shift the URL's data into a temporary file and then process it. + + File temporaryFile = null; + InputStream urlInputStream = null; + + try { + + urlInputStream = url.openStream(); + temporaryFile = File.createTempFile(job.getCode()+"__import",".hpkr"); + final InputStream finalUrlInputStream = urlInputStream; + + com.google.common.io.Files.copy( + new InputSupplier() { + @Override + public InputStream getInput() throws IOException { + return finalUrlInputStream; + } + }, + temporaryFile); + + logger.info("did copy data for repository {} ({}) to temporary file",job.getCode(),url.toString()); + + org.haikuos.pkg.HpkrFileExtractor fileExtractor = new org.haikuos.pkg.HpkrFileExtractor(temporaryFile); + PkgIterator pkgIterator = new PkgIterator(fileExtractor.getPackageAttributesIterator()); + + long startTimeMs = System.currentTimeMillis(); + logger.info("will process data for repository {}",job.getCode()); + + while(pkgIterator.hasNext()) { + importPackageService.run(repository.getObjectId(), pkgIterator.next()); + } + + logger.info("did process data for repository {} in {}ms",job.getCode(),System.currentTimeMillis()-startTimeMs); + + } + catch(Throwable th) { + logger.error("a problem has arisen processing a repository file for repository "+job.getCode()+" from url '"+url.toString()+"'",th); + } + finally { + Closeables.closeQuietly(urlInputStream); + + if(null!=temporaryFile && temporaryFile.exists()) { + temporaryFile.delete(); + } + } + + } + + /** + *

This is the object that gets enqueued to actually do the work.

+ */ + + public static class ImportRepositoryDataJobRunnable implements Runnable { + + private ImportRepositoryDataJob job; + + private ImportRepositoryDataService service; + + public ImportRepositoryDataJobRunnable( + ImportRepositoryDataService service, + ImportRepositoryDataJob job) { + Preconditions.checkNotNull(service); + Preconditions.checkNotNull(job); + this.service = service; + this.job = job; + } + + @Override + public void run() { + service.run(job); + } + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/SearchPkgsService.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/SearchPkgsService.java new file mode 100644 index 00000000..bea68745 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/SearchPkgsService.java @@ -0,0 +1,138 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.services; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.ejbql.parser.EJBQL; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.EJBQLQuery; +import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.query.SortOrder; +import org.haikuos.haikudepotserver.model.Architecture; +import org.haikuos.haikudepotserver.model.Pkg; +import org.haikuos.haikudepotserver.model.PkgVersion; +import org.haikuos.haikudepotserver.services.model.SearchPkgsSpecification; +import org.haikuos.haikudepotserver.services.model.SearchPkgsSpecification; +import org.haikuos.haikudepotserver.support.cayenne.LikeHelper; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; + +@Service +public class SearchPkgsService { + + public List search(ObjectContext context, SearchPkgsSpecification search) { + Preconditions.checkNotNull(search); + Preconditions.checkNotNull(context); + Preconditions.checkState(search.getOffset() >= 0); + Preconditions.checkState(search.getLimit() > 0); + Preconditions.checkNotNull(search.getArchitectures()); + Preconditions.checkState(!search.getArchitectures().isEmpty()); + + List pkgNames; + + // using jpql because of need to get out raw rows for the pkg name. + + { + StringBuilder queryBuilder = new StringBuilder(); + List parameters = Lists.newArrayList(); + List architecturesList = Lists.newArrayList(search.getArchitectures()); + + queryBuilder.append("SELECT DISTINCT pv.pkg.name FROM PkgVersion pv WHERE"); + + queryBuilder.append(" ("); + + for(int i=0; i < architecturesList.size(); i++) { + if(0!=i) { + queryBuilder.append(" OR"); + } + + queryBuilder.append(String.format(" pv.architecture.code = ?%d",parameters.size()+1)); + parameters.add(architecturesList.get(i).getCode()); + } + + queryBuilder.append(")"); + + queryBuilder.append(" AND"); + queryBuilder.append(" pv.active = true"); + queryBuilder.append(" AND"); + queryBuilder.append(" pv.pkg.active = true"); + + if(!Strings.isNullOrEmpty(search.getExpression())) { + queryBuilder.append(" AND"); + queryBuilder.append(String.format(" pv.pkg.name LIKE ?%d",parameters.size()+1)); + parameters.add("%" + LikeHelper.escapeExpression(search.getExpression()) + "%"); + } + + queryBuilder.append(" ORDER BY pv.pkg.name ASC"); + + EJBQLQuery query = new EJBQLQuery(queryBuilder.toString()); + + for(int i=0;i() { +// @Override +// public String apply(Architecture architecture) { +// return architecture.getCode(); +// } +// } +// )); +// +// if(!Strings.isNullOrEmpty(search.getExpression())) { +// query.setParameter("pkgNameLikeExpression", "%" + LikeHelper.escapeExpression(search.getExpression()) + "%"); +// } + + query.setFetchOffset(search.getOffset()); + query.setFetchLimit(search.getLimit()); + + pkgNames = (List) context.performQuery(query); + } + + List pkgs = Collections.emptyList(); + + if(0!=pkgNames.size()) { + SelectQuery query = new SelectQuery(Pkg.class, ExpressionFactory.inExp(Pkg.NAME_PROPERTY, pkgNames)); + pkgs = context.performQuery(query); + } + + return pkgs; + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/model/ImportRepositoryDataJob.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/model/ImportRepositoryDataJob.java new file mode 100644 index 00000000..626b742e --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/model/ImportRepositoryDataJob.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.services.model; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +import java.net.URL; + +/** + *

This object models the request to pull a repository's data into the database locally. See + * {@link org.haikuos.haikudepotserver.services.ImportPackageService} for further detail on this. + *

+ */ + +public class ImportRepositoryDataJob { + + private String code; + + public ImportRepositoryDataJob(String code) { + super(); + Preconditions.checkNotNull(code); + Preconditions.checkState(!Strings.isNullOrEmpty(code)); + this.code = code; + } + + public String getCode() { + return code; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ImportRepositoryDataJob that = (ImportRepositoryDataJob) o; + + if (!code.equals(that.code)) return false; + + return true; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public String toString() { + return getCode(); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/model/SearchPkgsSpecification.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/model/SearchPkgsSpecification.java new file mode 100644 index 00000000..507d145e --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/services/model/SearchPkgsSpecification.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.services.model; + +import org.haikuos.haikudepotserver.model.Architecture; + +import java.util.Collection; + +/** + *

This model object specifies the parameters of a search into the system for packages. See the + * {@link org.haikuos.haikudepotserver.services.SearchPkgsService} for further detail on this.

+ */ + +public class SearchPkgsSpecification { + + public enum ExpressionType { + CONTAINS + } + + private String expression; + + private Collection architectures; + + private ExpressionType expressionType; + + private int offset; + + private int limit; + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int value) { + this.limit = value; + } + + public String getExpression() { + return expression; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public ExpressionType getExpressionType() { + return expressionType; + } + + public void setExpressionType(ExpressionType expressionType) { + this.expressionType = expressionType; + } + + public Collection getArchitectures() { + return architectures; + } + + public void setArchitectures(Collection architectures) { + this.architectures = architectures; + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/Closeables.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/Closeables.java new file mode 100644 index 00000000..4d82de81 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/Closeables.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support; + +import java.io.Closeable; +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + *

Quite often in finally clauses you need to close off some resource such as a database + * {@link java.sql.Connection} or an {@link java.io.InputStream} or some such thing. This + * class contains some helpers for doing this without throwing more exceptions.

+ */ + +public class Closeables { + + public static void closeQuietly(Closeable closeable) { + if(null!=closeable) { + try { + closeable.close(); + } + catch(IOException ioe) { + // ignore. + } + } + } + + public static void closeQuietly(Connection connection) { + if(null!=connection) { + try { + connection.close(); + } + catch(SQLException e) { + // ignore + } + } + } + + public static void closeQuietly(PreparedStatement preparedStatement) { + if(null!=preparedStatement) { + try { + preparedStatement.close(); + } + catch(SQLException e) { + // ignore + } + } + } + + public static void closeQuietly(ResultSet resultSet) { + if(null!=resultSet) { + try { + resultSet.close(); + } + catch(SQLException e) { + // ignore + } + } + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/ConfigureDataSourceModule.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/ConfigureDataSourceModule.java new file mode 100644 index 00000000..3ce20811 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/ConfigureDataSourceModule.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.cayenne; + +import com.google.common.base.Preconditions; +import org.apache.cayenne.configuration.DataNodeDescriptor; +import org.apache.cayenne.configuration.server.DataSourceFactory; +import org.apache.cayenne.di.Binder; +import org.apache.cayenne.di.Module; + +import javax.sql.DataSource; + +/** + *

This object exists to get the data source injected into it and then to pass that onto the Cayenne environment. + *

+ */ + +public class ConfigureDataSourceModule implements Module { + + private DataSource dataSource; + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public void configure(Binder binder) { + Preconditions.checkNotNull(dataSource); + binder.bind(DataSourceFactory.class).toInstance(new FixedDataSourceFactory(dataSource)); + } + + public static class FixedDataSourceFactory implements DataSourceFactory { + + private DataSource dataSource; + + public FixedDataSourceFactory(DataSource dataSource) { + Preconditions.checkNotNull(dataSource); + this.dataSource = dataSource; + } + + @Override + public javax.sql.DataSource getDataSource(DataNodeDescriptor dataNodeDescriptor) { + return dataSource; + } + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/LikeHelper.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/LikeHelper.java new file mode 100644 index 00000000..29855de7 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/LikeHelper.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.cayenne; + +public class LikeHelper { + + public static char CHAR_ESCAPE = '|'; + + public static String escapeExpression(String value) { + return value.replace("%","|%").replace("_","|_"); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/PostAddCreateAndModifyTimestampListener.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/PostAddCreateAndModifyTimestampListener.java new file mode 100644 index 00000000..d10a69f1 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/cayenne/PostAddCreateAndModifyTimestampListener.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.cayenne; + +import org.haikuos.haikudepotserver.model.support.CreateAndModifyTimestamped; + +import java.util.Date; + +/** + *

This automates the configuration of the create and modify timestamps against certain + * entities that support the {@link CreateAndModifyTimestamped} interface.

+ */ + +public class PostAddCreateAndModifyTimestampListener { + + public void onPostAdd(Object entity) { + CreateAndModifyTimestamped createAndModifyTimestamped = (CreateAndModifyTimestamped) entity; + Date now = new Date(); + createAndModifyTimestamped.setCreateTimestamp(now); + createAndModifyTimestamped.setModifyTimestamp(now); + } + + public void onPreUpdate(Object entity) { + CreateAndModifyTimestamped createAndModifyTimestamped = (CreateAndModifyTimestamped) entity; + createAndModifyTimestamped.setModifyTimestamp(new Date()); + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/db/migration/FlywayMigrationOrchestration.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/db/migration/FlywayMigrationOrchestration.java new file mode 100644 index 00000000..f6e3efb1 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/db/migration/FlywayMigrationOrchestration.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.db.migration; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.googlecode.flyway.core.Flyway; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; + +/** + *

This object takes responsibility for setting up the database in the first place and also + * ensuring that any database migrations are applied before the application finishes starting-up. + * The SQL files to run the migrations are in the resources of this project. The system uses the + * flyway project to achieve this.

+ */ + +public class FlywayMigrationOrchestration { + + protected static Logger logger = LoggerFactory.getLogger(FlywayMigrationOrchestration.class); + + private DataSource dataSource; + + private boolean migrate = false; + + private String schema; + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public DataSource getDataSource() { + return dataSource; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + public boolean isMigrate() { + return migrate; + } + + public void setMigrate(boolean migrate) { + this.migrate = migrate; + } + + public void init() { + + Preconditions.checkNotNull(getDataSource()); + Preconditions.checkState(!Strings.isNullOrEmpty(getSchema())); + + Flyway flyway = new Flyway(); + flyway.setSchemas(getSchema()); + flyway.setLocations(String.format("db/%s/migration",getSchema())); + flyway.setDataSource(dataSource); + + logger.info("will migrate database to latest version..."); + flyway.migrate(); + logger.info("did migrate database to latest version..."); + + } + +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/WebResourceGroupTag.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/WebResourceGroupTag.java new file mode 100644 index 00000000..bebee8d5 --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/WebResourceGroupTag.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013, 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.support.web.model.WebResourceGroup; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.servlet.tags.form.AbstractHtmlElementTag; +import org.springframework.web.servlet.tags.form.TagWriter; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.JspException; +import java.io.IOException; + +/** + *

This is a JSP tag that is able to be inserted into the "single page" that can render out references + * to {@link WebResourceGroup}s that have been configured in the application context. These are rendered + * as, for example, "script" tags in the case of JavaScript resources. The tag is also able to reference + * the resources by a single URL in which case the application server will respond with a single response + * that contains all of the resources concatenated to avoid having to make many HTTP requests to the + * application server to get those.

+ */ + +public class WebResourceGroupTag extends AbstractHtmlElementTag { + + private final static String JS = MediaType.JAVASCRIPT_UTF_8.type() + "/" + MediaType.JAVASCRIPT_UTF_8.subtype(); + private final static String CSS = MediaType.CSS_UTF_8.type() + "/" + MediaType.CSS_UTF_8.subtype(); + + private String scriptGroupCode; + + public String getCode() { + return scriptGroupCode; + } + + public void setCode(String value) { + this.scriptGroupCode = value; + } + + @Override + protected int writeTagContent(TagWriter tagWriter) throws JspException { + + ServletContext servletContext = pageContext.getServletContext(); + HttpServletResponse response = (HttpServletResponse) pageContext.getResponse(); + + ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext()); + WebResourceGroup webResourceGroup = ctx.getBean(getCode() + WebResourceGroup.SUFFIX_BEANNAME, WebResourceGroup.class); + + if(null==webResourceGroup) { + throw new JspException("unable to find the web resource group with the code '"+getCode()+"'"); + } + + // if they are to be separated then render a script tag for each one; otherwise just render one + // script tag that will join them all together. + + if(webResourceGroup.getSeparated()) { + + try { + pageContext.getOut().print("\n\n"); + + for(String resource : webResourceGroup.getResources()) { + + if(JS.equals(webResourceGroup.getMimeType())) { + tagWriter.startTag("script"); + tagWriter.writeAttribute("src",response.encodeURL(resource)); + tagWriter.endTag(true); + } + + if(CSS.equals(webResourceGroup.getMimeType())) { + tagWriter.startTag("link"); + tagWriter.writeAttribute("href",response.encodeURL(resource)); + tagWriter.writeAttribute("rel","stylesheet"); + tagWriter.endTag(); + } + + pageContext.getOut().print("\n"); + } + } + catch(IOException ioe) { + throw new JspException("unable to write out the separated script tags",ioe); + } + } + else { + if(JS.equals(webResourceGroup.getMimeType())) { + tagWriter.startTag("script"); + tagWriter.writeAttribute("src",response.encodeURL("/webresourcegroup/"+webResourceGroup.getCode())); + tagWriter.endTag(true); + } + + if(CSS.equals(webResourceGroup.getMimeType())) { + tagWriter.startTag("link"); + tagWriter.writeAttribute("href",response.encodeURL("/webresourcegroup/"+webResourceGroup.getCode())); + tagWriter.writeAttribute("rel","stylesheet"); + tagWriter.endTag(); + } + } + + return SKIP_BODY; + } +} diff --git a/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/model/WebResourceGroup.java b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/model/WebResourceGroup.java new file mode 100644 index 00000000..f32dad2c --- /dev/null +++ b/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/model/WebResourceGroup.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.support.web.model; + +import com.google.common.net.MediaType; + +import java.util.List; + +/** + *

This object identifies a number of web resources that can be grouped together. They are grouped in this way + * such that they can be referenced once in the application for download; either splayed out as individual requests + * or as a composite download.

+ */ + +public class WebResourceGroup { + + public final static String SUFFIX_BEANNAME = "WebResourceGroup"; + + private String code; + + private List resources; + + private String mimeType; + + private Boolean separated; + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mediaType) { + this.mimeType = mediaType; + } + + public Boolean getSeparated() { + return separated; + } + + public void setSeparated(Boolean separated) { + this.separated = separated; + } + + public List getResources() { + return resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + @Override + public String toString() { + return String.format("%s; %d web resources",getCode(),null!=getResources()?getResources().size():0); + } + +} diff --git a/haikudepotserver-webapp/src/main/resources/HaikuDepot.map.xml b/haikudepotserver-webapp/src/main/resources/HaikuDepot.map.xml new file mode 100644 index 00000000..e49af184 --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/HaikuDepot.map.xml @@ -0,0 +1,284 @@ + + + + + + + + + + + ORACLE + haikudepot.architecture_seq + 1 + + + + + + + + + + + ORACLE + haikudepot.pkg_seq + 10 + + + + + + + ORACLE + haikudepot.pkg_url_type_seq + 1 + + + + + + + + + + + + + + + + + ORACLE + haikudepot.pkg_version_seq + 10 + + + + + + + + ORACLE + haikudepot.pkg_version_copyright_seq + 10 + + + + + + + + ORACLE + haikudepot.pkg_version_license_seq + 10 + + + + + + + + + ORACLE + haikudepot.pkg_version_localization_seq + 10 + + + + + + + + + ORACLE + haikudepot.pkg_version_url_seq + 10 + + + + + + + + + + + + + ORACLE + haikudepot.publisher_seq + 1 + + + + + + + + + + + + ORACLE + haikudepot.repository_seq + 1 + + + + + + + + + + + + ORACLE + haikudepot.user_seq + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/haikudepotserver-webapp/src/main/resources/cayenne-haikudepotserver.xml b/haikudepotserver-webapp/src/main/resources/cayenne-haikudepotserver.xml new file mode 100644 index 00000000..150b89e8 --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/cayenne-haikudepotserver.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/haikudepotserver-webapp/src/main/resources/db/captcha/migration/V1.0__Initialize.sql b/haikudepotserver-webapp/src/main/resources/db/captcha/migration/V1.0__Initialize.sql new file mode 100644 index 00000000..3c4958df --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/db/captcha/migration/V1.0__Initialize.sql @@ -0,0 +1,12 @@ +-- ------------------------------------------------------ +-- SETUP CAPTCHA SCHEMA OBJECTS +-- ------------------------------------------------------ + +-- Flyway will take care of this. +-- CREATE SCHEMA captcha; + +CREATE TABLE captcha.responses (token CHAR(36) NOT NULL, response VARCHAR(255) NOT NULL, create_timestamp TIMESTAMP NOT NULL); +CREATE UNIQUE INDEX responses_idx01 ON captcha.responses(token); + + + diff --git a/haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.0__Initialize.sql b/haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.0__Initialize.sql new file mode 100644 index 00000000..c90d8fe2 --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/db/haikudepot/migration/V1.0__Initialize.sql @@ -0,0 +1,84 @@ +-- ------------------------------------------------------ +-- SETUP +-- ------------------------------------------------------ + +-- Flyway will take care of this. +-- CREATE SCHEMA haikudepot; + +-- ------------------------------------------------------ +-- TABLES GENERATED BY CAYENNE MODELLER +-- ------------------------------------------------------ + +CREATE TABLE haikudepot.pkg_url_type (code VARCHAR(255) NOT NULL, id BIGINT NOT NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.publisher (active BOOLEAN NOT NULL, code VARCHAR(255) NULL, create_timestamp TIMESTAMP NOT NULL, email VARCHAR(1024) NULL, id BIGINT NOT NULL, modify_timestamp TIMESTAMP NOT NULL, name VARCHAR(1024) NOT NULL, site_url VARCHAR(1024) NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.architecture (code VARCHAR(255) NOT NULL, id BIGINT NOT NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.pkg (active BOOLEAN NOT NULL, create_timestamp TIMESTAMP NOT NULL, id BIGINT NOT NULL, modify_timestamp TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, publisher_id BIGINT NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.repository (active BOOLEAN NOT NULL, architecture_id BIGINT NOT NULL, code VARCHAR(255) NOT NULL, create_timestamp TIMESTAMP NOT NULL, id BIGINT NOT NULL, modify_timestamp TIMESTAMP NOT NULL, url VARCHAR(1024) NOT NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.pkg_version (active BOOLEAN NOT NULL, architecture_id BIGINT NOT NULL, create_timestamp TIMESTAMP NOT NULL, id BIGINT NOT NULL, major VARCHAR(255) NOT NULL, micro VARCHAR(255) NULL, minor VARCHAR(255) NULL, modify_timestamp TIMESTAMP NOT NULL, pkg_id BIGINT NOT NULL, pre_release VARCHAR(255) NULL, repository_id BIGINT NOT NULL, revision INTEGER NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.pkg_version_copyright (body VARCHAR(4096) NOT NULL, id BIGINT NOT NULL, pkg_version_id BIGINT NOT NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.pkg_version_localization (description VARCHAR(8192) NULL, id BIGINT NOT NULL, pkg_version_id BIGINT NULL, summary VARCHAR(8192) NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.pkg_version_license (body VARCHAR(4096) NOT NULL, id BIGINT NOT NULL, pkg_version_id BIGINT NOT NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.pkg_version_url (id BIGINT NOT NULL, pkg_url_type_id BIGINT NOT NULL, pkg_version_id BIGINT NOT NULL, url VARCHAR(1024) NOT NULL, PRIMARY KEY (id)); +CREATE TABLE haikudepot.user (active BOOLEAN NOT NULL, can_manage_users BOOLEAN NOT NULL, id BIGINT NOT NULL, is_root BOOLEAN NOT NULL, nickname VARCHAR(32) NOT NULL, password_hash VARCHAR(255) NOT NULL, password_salt VARCHAR(255) NOT NULL, PRIMARY KEY (id)); + +ALTER TABLE haikudepot.pkg ADD FOREIGN KEY (publisher_id) REFERENCES haikudepot.publisher (id); +ALTER TABLE haikudepot.repository ADD FOREIGN KEY (architecture_id) REFERENCES haikudepot.architecture (id); +ALTER TABLE haikudepot.pkg_version ADD FOREIGN KEY (architecture_id) REFERENCES haikudepot.architecture (id); +ALTER TABLE haikudepot.pkg_version ADD FOREIGN KEY (pkg_id) REFERENCES haikudepot.pkg (id); +ALTER TABLE haikudepot.pkg_version ADD FOREIGN KEY (repository_id) REFERENCES haikudepot.repository (id); +ALTER TABLE haikudepot.pkg_version_copyright ADD FOREIGN KEY (pkg_version_id) REFERENCES haikudepot.pkg_version (id); +ALTER TABLE haikudepot.pkg_version_localization ADD FOREIGN KEY (pkg_version_id) REFERENCES haikudepot.pkg_version (id); +ALTER TABLE haikudepot.pkg_version_license ADD FOREIGN KEY (pkg_version_id) REFERENCES haikudepot.pkg_version (id); +ALTER TABLE haikudepot.pkg_version_url ADD FOREIGN KEY (pkg_url_type_id) REFERENCES haikudepot.pkg_url_type (id); +ALTER TABLE haikudepot.pkg_version_url ADD FOREIGN KEY (pkg_version_id) REFERENCES haikudepot.pkg_version (id); + +-- ------------------------------------------------------ +-- MANUALLY CREATED SEQUENCES +-- ------------------------------------------------------ + +CREATE SEQUENCE haikudepot.repository_seq START WITH 7629 INCREMENT BY 1; +CREATE SEQUENCE haikudepot.architecture_seq START WITH 2862 INCREMENT BY 1; +CREATE SEQUENCE haikudepot.pkg_seq START WITH 9812 INCREMENT BY 10; +CREATE SEQUENCE haikudepot.pkg_version_url_seq START WITH 8364 INCREMENT BY 10; +CREATE SEQUENCE haikudepot.pkg_version_seq START WITH 2736 INCREMENT BY 10; +CREATE SEQUENCE haikudepot.pkg_version_localization_seq START WITH 4422 INCREMENT BY 10; +CREATE SEQUENCE haikudepot.pkg_version_copyright_seq START WITH 8684 INCREMENT BY 10; +CREATE SEQUENCE haikudepot.pkg_version_license_seq START WITH 3826 INCREMENT BY 10; +CREATE SEQUENCE haikudepot.pkg_url_type_seq START WITH 2235 INCREMENT BY 1; +CREATE SEQUENCE haikudepot.publisher_seq START WITH 9373 INCREMENT BY 1; +CREATE SEQUENCE haikudepot.user_seq START WITH 1247 INCREMENT BY 1; + +-- ------------------------------------------------------ +-- REFERENCE DATA +-- ------------------------------------------------------ + +INSERT INTO haikudepot.architecture (id,code) VALUES (nextval('architecture_seq'), 'x86_gcc2'); +INSERT INTO haikudepot.architecture (id,code) VALUES (nextval('architecture_seq'), 'x86'); +INSERT INTO haikudepot.architecture (id,code) VALUES (nextval('architecture_seq'), 'any'); +INSERT INTO haikudepot.architecture (id,code) VALUES (nextval('architecture_seq'), 'source'); + +INSERT INTO haikudepot.pkg_url_type (id,code) VALUES (nextval('pkg_url_type_seq'), 'homepage'); + +-- ------------------------------------------------------ +-- INDEXES +-- ------------------------------------------------------ + +CREATE UNIQUE INDEX architecture_idx01 ON haikudepot.architecture(code); +CREATE UNIQUE INDEX repository_idx01 ON haikudepot.repository(code); +CREATE UNIQUE INDEX pkg_idx01 ON haikudepot.pkg(name); +CREATE UNIQUE INDEX pkg_url_type_idx01 ON haikudepot.pkg_url_type(code); +CREATE UNIQUE INDEX publisher_idx01 ON haikudepot.publisher(code); +CREATE UNIQUE INDEX version_idx01 ON haikudepot.pkg_version(pkg_id, major, minor, micro, revision, pre_release); +CREATE UNIQUE INDEX user_idx01 ON haikudepot.user(nickname); + +CREATE INDEX pkg_idx02 ON haikudepot.pkg(publisher_id); +CREATE INDEX pkg_version_idx02 ON haikudepot.pkg_version(repository_id); +CREATE INDEX pkg_version_idx03 ON haikudepot.pkg_version(architecture_id); +CREATE INDEX pkg_version_copyright_idx01 ON haikudepot.pkg_version_copyright(pkg_version_id); +CREATE INDEX pkg_version_license_idx01 ON haikudepot.pkg_version_license(pkg_version_id); +CREATE INDEX pkg_version_localization_idx01 ON haikudepot.pkg_version_localization(pkg_version_id); +CREATE INDEX pkg_version_url_idx01 ON haikudepot.pkg_version_url(pkg_url_type_id); +CREATE INDEX pkg_version_url_idx03 ON haikudepot.pkg_version_url(pkg_version_id); +CREATE INDEX repository_idx02 ON haikudepot.repository(architecture_id); + +-- ------------------------------------------------------ diff --git a/haikudepotserver-webapp/src/main/resources/local-sample.properties b/haikudepotserver-webapp/src/main/resources/local-sample.properties new file mode 100644 index 00000000..e3668d3e --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/local-sample.properties @@ -0,0 +1,15 @@ +# This is a simple properties file for the application. It is suggested that this file be copied as "local.properties" +# in the same directory for development purposes. + +# Database connection detail +jdbc.driver=org.postgresql.Driver +jdbc.url=jdbc:postgresql://localhost:5432/haikudepotserver +jdbc.username=someuser +jdbc.password=somepassword + +# Set to true if you want the database to be upgraded to the correct version on startup. +flyway.migrate=true + +# Set to true if you would like javascript and CSS resources to be rendered in the HTML page as separate links +# as opposed to a single HTTP request. Separated is easier to work with from a development perspective. +webresourcegroup.separated=true \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/resources/logback.xml b/haikudepotserver-webapp/src/main/resources/logback.xml new file mode 100644 index 00000000..3f62188c --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/logback.xml @@ -0,0 +1,22 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/resources/messages.properties b/haikudepotserver-webapp/src/main/resources/messages.properties new file mode 100644 index 00000000..8e5a414b --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/messages.properties @@ -0,0 +1,12 @@ +# These are localization messages that appear in the application + +createUser.nickname.required=The nickname of the new user is required. +createUser.nickname.pattern=The nickname can consist of latin characters and digits only and must be between 4 and 16 characters in length. +createUser.nickname.notunique=The nickname is already in-use; nominate an alternative nickname. +createUser.passwordClear.required=The password of the new user is required. +createUser.passwordClear.minlength=The password must be at least 5 characters long. +createUserForm.captchaResponse.required=The response to the question in the image is required to ensure that the registration is from a human operator. +createUserForm.captchaResponse.badresponse=The response supplied does not match the question in the image or the question has expired; a new image has been provided. + +authenticateUser.nickname.required=The nickname is required to login. +authenticateUser.passwordClear.required=The password is required to login. \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/resources/spring/application-context.xml b/haikudepotserver-webapp/src/main/resources/spring/application-context.xml new file mode 100644 index 00000000..cb7d8323 --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/spring/application-context.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + classpath:local.properties + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/resources/spring/persistence-context.xml b/haikudepotserver-webapp/src/main/resources/spring/persistence-context.xml new file mode 100644 index 00000000..44cf143d --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/spring/persistence-context.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/resources/spring/servlet-context.xml b/haikudepotserver-webapp/src/main/resources/spring/servlet-context.xml new file mode 100644 index 00000000..4eb9f762 --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/spring/servlet-context.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + /WEB-INF/views/ + + + .jsp + + + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/resources/spring/webresourcegroup-context.xml b/haikudepotserver-webapp/src/main/resources/spring/webresourcegroup-context.xml new file mode 100644 index 00000000..42e749a3 --- /dev/null +++ b/haikudepotserver-webapp/src/main/resources/spring/webresourcegroup-context.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + /js/lib/base64-min.js + /js/lib/moment-min.js + /js/lib/underscore-min.js + /js/lib/angular-min.js + /js/lib/angular-route-min.js + /js/lib/ui-bootstrap-tpls.js + /js/lib/angular-ui.js + /js/lib/base64-min.js + + + + + + + + + + + + + /js/app/haikudepotserver.js + /js/app/routes.js + /js/app/constants.js + + /js/app/directive/bannerdirective.js + /js/app/directive/spinnerdirective.js + /js/app/directive/versionlabeldirective.js + /js/app/directive/breadcrumbsdirective.js + /js/app/directive/messagedirective.js + /js/app/directive/errormessagesdirective.js + + /js/app/controller/viewpkgcontroller.js + /js/app/controller/homecontroller.js + /js/app/controller/errorcontroller.js + /js/app/controller/createusercontroller.js + /js/app/controller/viewusercontroller.js + /js/app/controller/authenticateusercontroller.js + + /js/app/service/jsonrpcservice.js + /js/app/service/messagesourceservice.js + /js/app/service/userstateservice.js + /js/app/service/referencedataservice.js + + + + + + + + + + + + + + /css/haikudepotserver.css + /css/home.css + /css/createuser.css + + + + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/WEB-INF/haikudepotserver.tld b/haikudepotserver-webapp/src/main/webapp/WEB-INF/haikudepotserver.tld new file mode 100644 index 00000000..579c8717 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/WEB-INF/haikudepotserver.tld @@ -0,0 +1,20 @@ + + + + 1.0 + 1.2 + haikudepotserver + + + webresourcegroup + org.haikuos.haikudepotserver.support.web.WebResourceGroupTag + + This tag can be used to include a script group on the page; either combined into one script element or as + a set of separate script elements; contingent on the configuration of the script group. + + + code + true + + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/WEB-INF/views/entryPoint.jsp b/haikudepotserver-webapp/src/main/webapp/WEB-INF/views/entryPoint.jsp new file mode 100644 index 00000000..51d4e585 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/WEB-INF/views/entryPoint.jsp @@ -0,0 +1,33 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="hds" uri="/WEB-INF/haikudepotserver.tld" %> + +<%-- +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. +--%> + + + + + + Haiku Depot Web + + + + + + + + + + + + + + +
+
+
+ + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/WEB-INF/web.xml b/haikudepotserver-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..9c5a060c --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,30 @@ + + + Spring Web MVC Application + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:/spring/servlet-context.xml + + 1 + + + + mvc-dispatcher + / + + + + contextConfigLocation + classpath:spring/application-context.xml + + + + org.springframework.web.context.ContextLoaderListener + + diff --git a/haikudepotserver-webapp/src/main/webapp/bootstrap/README.TXT b/haikudepotserver-webapp/src/main/webapp/bootstrap/README.TXT new file mode 100644 index 00000000..f42b7b5b --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/bootstrap/README.TXT @@ -0,0 +1,3 @@ +This directory contains Bootstrap materials that are required for this application. These materials will be downloaded +into this directory the first time that the application is built; using an ant script that is referenced from the maven +build process. \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/css/createuser.css b/haikudepotserver-webapp/src/main/webapp/css/createuser.css new file mode 100644 index 00000000..45938232 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/css/createuser.css @@ -0,0 +1,4 @@ +#create-user-captcha-response-input { + width: 64px; + display: inline; +} \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/css/haikudepotserver.css b/haikudepotserver-webapp/src/main/webapp/css/haikudepotserver.css new file mode 100644 index 00000000..792d38fe --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/css/haikudepotserver.css @@ -0,0 +1,42 @@ +/* +These styles are related to the application as a whole. +*/ + +body { + padding-top: 50px; +} + +.table > thead > tr { + font-weight: bold; +} + +.generic-page-content-container { + margin-top: 16px; +} + +.container { + max-width: none; +} + +/* +This material is for the spinner; it will fill the screen will white and then put a small animation on top for +a moment. +*/ + +#spinnerbackdrop { + position: fixed; + left: 0px; + top: 0px; + bottom: 0px; + right: 0px; + background-color: rgba(255, 255, 255, 0.90); + z-index: 1050; +} + +#spinnerbackdrop > div { + width: 120px; + margin: 0 auto; + padding-top: 120px; +} + + diff --git a/haikudepotserver-webapp/src/main/webapp/css/home.css b/haikudepotserver-webapp/src/main/webapp/css/home.css new file mode 100644 index 00000000..c3146ca9 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/css/home.css @@ -0,0 +1,47 @@ +/* +These styles are related to the 'home page' of the application. +*/ + +#home-search-criteria-bar > div { + background-color: rgb(253,207,49); + min-height: 44px; + text-align: center; + padding-top: 12px; +} + +#home-search-criteria-bar-categories > span { + color: rgba(0, 0, 0, 0.50); +} + +/* +Special layout for the search field which is in the 3rd box +*/ + +#home-search-criteria-bar #home-search-criteria-bar-search { + padding-top: 4px; + padding-bottom: 4px; + padding-left: 8px; + padding-right: 8px; +} + +#home-search-criteria-bar > div.selected { + background-color: white; + border-bottom: white 1px solid; +} + +#home-search-criteria-bar > div:not(.selected) { + border-bottom: black 1px solid; +} + +#home-search-criteria-bar > div:nth-child(n+2) { + border-left: black 1px solid; +} + +#home-search-results-container { + margin-top: 16px; + padding: 16px; +} + +#home-search-results-container > table > tbody > tr > td:nth-child(1) { + width: 24px; +} \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/img/favicon.png b/haikudepotserver-webapp/src/main/webapp/img/favicon.png new file mode 100644 index 00000000..67f38ac6 Binary files /dev/null and b/haikudepotserver-webapp/src/main/webapp/img/favicon.png differ diff --git a/haikudepotserver-webapp/src/main/webapp/img/spinner.svg b/haikudepotserver-webapp/src/main/webapp/img/spinner.svg new file mode 100644 index 00000000..a3c501db --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/img/spinner.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/constants.js b/haikudepotserver-webapp/src/main/webapp/js/app/constants.js new file mode 100644 index 00000000..1ec91067 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/constants.js @@ -0,0 +1,28 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').constant('constants', { + + ARCHITECTURE_CODE_DEFAULT : 'x86', + + ENDPOINT_API_V1_PKG : '/api/v1/pkg', + ENDPOINT_API_V1_CAPTCHA : '/api/v1/captcha', + ENDPOINT_API_V1_MISCELLANEOUS : '/api/v1/miscellaneous', + ENDPOINT_API_V1_USER : '/api/v1/user', + + /** + *

This function expects to be supplied a JSON-RPC error object and will then direct the user to + * an error page from where they can return into the application again.

+ */ + + ERRORHANDLING_JSONRPC : function(err,$location,$log) { + if($log) { + $log.error('an error has arisen in invoking a json-rpc method'); + } + + $location.path("/error").search({}); + } + } +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateuser.html b/haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateuser.html new file mode 100644 index 00000000..5c46c180 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateuser.html @@ -0,0 +1,54 @@ +
+ + + +
+ Login Failed! + Your login attempt has failed; try again. +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+ +
+ +
+ + + diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateusercontroller.js b/haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateusercontroller.js new file mode 100644 index 00000000..ac848bb5 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateusercontroller.js @@ -0,0 +1,99 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').controller( + 'AuthenticateUserController', + [ + '$scope','$log','$location', + 'jsonRpc','constants','userState', + function( + $scope,$log,$location, + jsonRpc,constants,userState) { + + if(userState.user()) { + throw 'it is not possible to enter the authenticate user controller with a currently authenticated user'; + } + + $scope.didFailAuthentication = false; + $scope.amAuthenticating = false; + $scope.authenticationDetails = { + nickname : undefined, + passwordClear : undefined + }; + $scope.breadcrumbItems = [{ + title : 'Login', + path : $location.path() + }]; + + $scope.shouldSpin = function() { + return $scope.amAuthenticating; + } + + $scope.deriveFormControlsContainerClasses = function(name) { + return $scope.authenticateUserForm[name].$invalid ? ['has-error'] : []; + } + + // This function will take the data from the form and will authenticate + // the user from this data. + + $scope.goAuthenticate = function() { + + if($scope.authenticateUserForm.$invalid) { + throw 'expected the authentication of a user only to be possible if the form is valid'; + } + + $scope.didFailAuthentication = false; + $scope.amAuthenticating = true; + + jsonRpc.call( + constants.ENDPOINT_API_V1_USER, + "authenticateUser", + [{ + nickname : $scope.authenticationDetails.nickname, + passwordClear : $scope.authenticationDetails.passwordClear + }] + ).then( + function(result) { + if(result.authenticated) { + + userState.user({ + nickname : $scope.authenticationDetails.nickname, + passwordClear : $scope.authenticationDetails.passwordClear + }) + + $log.info('successful authentication; '+$scope.authenticationDetails.nickname); + + // either the user specified where they want to return to + // of we just take them back to their home page. + + var destination = $location.search()['destination']; + + if(destination && 0!=destination.length) { + $location.path(destination).search({}); + } + else { + $location.path('/').search({}); + } + } + else { + $log.info('failed authentication; '+$scope.authenticationDetails.nickname); + $scope.didFailAuthentication = true; + $scope.authenticationDetails.passwordClear = undefined; + } + + $scope.amAuthenticating = false; + }, + function(err) { + $scope.amAuthenticating = false; + constants.ERRORHANDLING_JSONRPC(err,$location,$log); + } + ); + + + } + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/createuser.html b/haikudepotserver-webapp/src/main/webapp/js/app/controller/createuser.html new file mode 100644 index 00000000..b0b63ace --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/createuser.html @@ -0,0 +1,70 @@ +
+ + + +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + = + + +
+
+ +
+
+ +
+
+ +
+ +
+ + + diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/createusercontroller.js b/haikudepotserver-webapp/src/main/webapp/js/app/controller/createusercontroller.js new file mode 100644 index 00000000..ce65a03d --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/createusercontroller.js @@ -0,0 +1,149 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').controller( + 'CreateUserController', + [ + '$scope','$log','$location', + 'jsonRpc','constants', + function( + $scope,$log,$location, + jsonRpc,constants) { + + $scope.breadcrumbItems = undefined; + $scope.captchaToken = undefined; + $scope.captchaImageUrl = undefined; + $scope.amSaving = false; + $scope.newUser = { + nickname : undefined, + passwordClear : undefined, + captchaResponse : undefined + }; + + regenerateCaptcha(); + + $scope.shouldSpin = function() { + return undefined == $scope.captchaToken || $scope.amSaving; + } + + $scope.deriveFormControlsContainerClasses = function(name) { + return $scope.createUserForm[name].$invalid ? ['has-error'] : []; + } + + function refreshBreadcrumbItems() { + $scope.breadcrumbItems = [{ + title : 'Create User', + path : $location.path() + }]; + } + + function regenerateCaptcha() { + + $scope.captchaToken = undefined; + $scope.captchaImageUrl = undefined; + $scope.newUser.captchaResponse = undefined; + + jsonRpc.call( + constants.ENDPOINT_API_V1_CAPTCHA, + "generateCaptcha", + [{}] + ).then( + function(result) { + $scope.captchaToken = result.token; + $scope.captchaImageUrl = 'data:image/png;base64,'+result.pngImageDataBase64; + refreshBreadcrumbItems(); + }, + function(err) { + constants.ERRORHANDLING_JSONRPC(err,$location,$log); + } + ); + } + + // When you go to save, if the user types the wrong captcha response then they will get an error message + // letting them know this, but there is no natural mechanism for this invalid state to get unset. For + // this reason, any change to the response text field will be taken to trigger this error state to be + // removed. + + $scope.captchaResponseDidChange = function() { + $scope.createUserForm.captchaResponse.$setValidity('badresponse',true); + } + + $scope.nicknameDidChange = function() { + $scope.createUserForm.nickname.$setValidity('notunique',true); + } + + // This function will take the data from the form and will create the user from this data. + + $scope.goCreateUser = function() { + + if($scope.createUserForm.$invalid) { + throw 'expected the creation of a user only to be possible if the form is valid'; + } + + $scope.amSaving = true; + + jsonRpc.call( + constants.ENDPOINT_API_V1_USER, + "createUser", + [{ + nickname : $scope.newUser.nickname, + passwordClear : $scope.newUser.passwordClear, + captchaToken : $scope.captchaToken, + captchaResponse : $scope.newUser.captchaResponse + }] + ).then( + function(result) { + $log.info('created new user; '+$scope.newUser.nickname); + $location.path('/viewuser/'+$scope.newUser.nickname).search({}); + }, + function(err) { + + regenerateCaptcha(); + $scope.amSaving = false; + + switch(err.code) { + + case jsonRpc.errorCodes.VALIDATION: + + // actually there shouldn't really be any validation problems except that the nickname + // may already be in use. We can deal with this one and then pass the rest to the + // default handler. + + if(err.data && err.data.validationfailures) { + var nicknameFailure = _.find(err.data.validationfailures, function(e) { return e.property == 'nickname'; }); + + if(nicknameFailure) { + $scope.createUserForm.nickname.$setValidity(nicknameFailure.message,false); + } + } + + if( + !err.data + || !err.data.validationfailures + || 0!=_.filter(err.data.validationfailures, function(e) { return e.property != 'nickname'; }).length) { + $log.error('other validation failures exist; will invoke default handling'); + constants.ERRORHANDLING_JSONRPC(err,$location,$log); + } + + break; + + case jsonRpc.errorCodes.CAPTCHABADRESPONSE: + $log.error('the user has mis-interpreted the captcha; will lodge an error into the form and then populate a new one for them'); + $scope.createUserForm.captchaResponse.$setValidity('badresponse',false); + break; + + default: + constants.ERRORHANDLING_JSONRPC(err,$location,$log); + break; + } + } + ); + + + } + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/error.html b/haikudepotserver-webapp/src/main/webapp/js/app/controller/error.html new file mode 100644 index 00000000..80bdd186 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/error.html @@ -0,0 +1,9 @@ +
+
+ Oh darn! + Something has gone wrong with your use of this web application; + you will have to + start again + . +
+
\ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/errorcontroller.js b/haikudepotserver-webapp/src/main/webapp/js/app/controller/errorcontroller.js new file mode 100644 index 00000000..c73b746d --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/errorcontroller.js @@ -0,0 +1,15 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').controller( + 'ErrorController', + [ + '$scope','$log','$location', + function( + $scope,$log,$location) { + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/home.html b/haikudepotserver-webapp/src/main/webapp/js/app/controller/home.html new file mode 100644 index 00000000..3d46a5af --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/home.html @@ -0,0 +1,70 @@ + +
+
Most Recent
+
Most Viewed
+
+ Category : + +
+ +
+ +
+ +
+ +
+ No results; + There were no packages able to be found from your search criteria. +
+ +
+ + + + + + + + + + + + + +
PackageCurrent Version
{{pkg.name}}
+ +
+
    +
  • + « +
  • + +
  • + » +
  • +
+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/homecontroller.js b/haikudepotserver-webapp/src/main/webapp/js/app/controller/homecontroller.js new file mode 100644 index 00000000..9c3ac578 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/homecontroller.js @@ -0,0 +1,205 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').controller( + 'HomeController', + [ + '$scope','$log','$location','jsonRpc','constants','userState', + function( + $scope,$log,$location,jsonRpc,constants,userState) { + + const PAGESIZE = 14; + + // This is just some sample data. In reality, this material would be obtained from the server and then + // cached locally. + + $scope.categories = [ + 'Category...', + 'Temporary', + 'Example', + 'Test', + 'Cases' + ]; + + $scope.ViewCriteriaTypes = { + SEARCH : 'SEARCH', + MOSTVIEWED : 'MOSTVIEWED', + CATEGORIES : 'CATEGORIES', + MOSTRECENT : 'MOSTRECENT' + }; + + // default model settings. + + $scope.viewCriteriaType = $scope.ViewCriteriaTypes.MOSTRECENT; + $scope.selectedCategory = $scope.categories[0]; + $scope.searchExpression = ''; + $scope.lastRefetchPkgsSearchExpression = ''; + $scope.pkgs = undefined; + $scope.hasMore = undefined; + $scope.offset = 0; + + refetchPkgsAtFirstPage(); + + // ---- WATCHES + // watch various values and react. + + $scope.$watch('selectedCategory', function(newValue) { + if(newValue !== $scope.categories[0]) { + $scope.goChooseCategory(); + } + }); + + $scope.shouldSpin = function() { + return undefined == $scope.pkgs; + } + + // ---- SEARCH CRITERION USER INTERFACE + // functions to control the user interface; not much generalization here as there will be some specific + // logic. + + function setViewCriteriaType(type) { + if($scope.viewCriteriaType != type) { + $scope.viewCriteriaType = type; + + if(type != $scope.ViewCriteriaTypes.SEARCH) { + $scope.searchExpression = ''; + } + + if(type != $scope.ViewCriteriaTypes.CATEGORIES) { + $scope.selectedCategory = $scope.categories[0]; + } + + refetchPkgsAtFirstPage(); + } + } + + $scope.classCategories = function() { + return $scope.viewCriteriaType == $scope.ViewCriteriaTypes.CATEGORIES ? 'selected' : null; + } + + $scope.classMostRecent = function() { + return $scope.viewCriteriaType == $scope.ViewCriteriaTypes.MOSTRECENT ? 'selected' : null; + } + + $scope.classMostViewed = function() { + return $scope.viewCriteriaType == $scope.ViewCriteriaTypes.MOSTVIEWED ? 'selected' : null; + } + + $scope.classSearch = function() { + return $scope.viewCriteriaType == $scope.ViewCriteriaTypes.SEARCH ? 'selected' : null; + } + + $scope.goMostRecent = function() { + setViewCriteriaType($scope.ViewCriteriaTypes.MOSTRECENT); + return false; + } + + $scope.goMostViewed = function() { + setViewCriteriaType($scope.ViewCriteriaTypes.MOSTVIEWED); + return false; + } + + $scope.goChooseCategory = function() { + setViewCriteriaType($scope.ViewCriteriaTypes.CATEGORIES); + return false; + } + + $scope.goSearchExpression = function() { + if($scope.viewCriteriaType == $scope.ViewCriteriaTypes.SEARCH) { + if($scope.lastRefetchPkgsSearchExpression != $scope.searchExpression) { + refetchPkgsAtFirstPage(); + } + } + else { + setViewCriteriaType($scope.ViewCriteriaTypes.SEARCH); + } + + return false; + } + + // ---- PAGINATION + + $scope.goPreviousPage = function() { + if($scope.offset > 0) { + $scope.offset -= PAGESIZE; + refetchPkgs(); + } + + return false; + } + + $scope.goNextPage = function() { + if($scope.hasMore) { + $scope.offset += PAGESIZE; + refetchPkgs(); + } + + return false; + } + + $scope.classPreviousPage = function() { + return $scope.offset > 0 ? [] : ['disabled']; + } + + $scope.classNextPage = function() { + return $scope.hasMore ? [] : ['disabled']; + } + + // ---- VIEW PKG + VERSION + + $scope.goViewPkg = function(pkg) { + $location.path('/viewpkg/'+pkg.name+'/latest').search({}); + return false; + } + + // ---- UPDATE THE RESULTS LOGIC + + function refetchPkgsAtFirstPage() { + $scope.offset = 0; + refetchPkgs(); + } + + // this function will pop off to the server and will pull-down the list of packages depending on what the + // user had selected in the criteria. + + function refetchPkgs() { + + $scope.pkgs = undefined; + $scope.lastRefetchPkgsSearchExpression = $scope.searchExpression; + + userState.architecture().then( + function(architecture) { + jsonRpc.call( + constants.ENDPOINT_API_V1_PKG, + "searchPkgs", + [{ + expression : $scope.searchExpression, + architectureCode : architecture.code, + expressionType : 'CONTAINS', + offset : $scope.offset, + limit : PAGESIZE + }] + ).then( + function(result) { + $scope.pkgs = result.pkgs; + $scope.hasMore = result.hasMore; + $log.info('found '+result.pkgs.length+' packages'); + }, + function(err) { + constants.ERRORHANDLING_JSONRPC(err,$location,$log); + } + ); + }, + function() { + $log.error('a problem has arisen getting the architecture'); + $location.path("/error").search({}); + } + ); + + + } + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html new file mode 100644 index 00000000..2dc22cd1 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html @@ -0,0 +1,25 @@ +
+ + + +

+ {{pkg.name}} + + + +

+ +

+ + + - {{pkg.versions[0].architectureCode}} + +

+ +

{{pkg.versions[0].summary}}

+

{{pkg.versions[0].description}}

+ +
+ + + diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js new file mode 100644 index 00000000..51c55748 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js @@ -0,0 +1,78 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').controller( + 'ViewPkgController', + [ + '$scope','$log','$location','$routeParams', + 'jsonRpc','constants','userState', + function( + $scope,$log,$location,$routeParams, + jsonRpc,constants,userState) { + + $scope.breadcrumbItems = undefined; + $scope.pkg = undefined; + + refetchPkg(); + + $scope.shouldSpin = function() { + return undefined == $scope.pkg; + } + + $scope.homePageLink = function() { + var u = undefined; + + if($scope.pkg) { + u = _.find( + $scope.pkg.versions[0].urls, + function(url) { + return url.urlTypeCode == 'homepage'; + }); + } + + return u ? u.url : undefined; + } + + function refreshBreadcrumbItems() { + $scope.breadcrumbItems = [{ + title : $scope.pkg.name, + path : $location.path() + }]; + } + + function refetchPkg() { + + userState.architecture().then( + function(architecture) { + jsonRpc.call( + constants.ENDPOINT_API_V1_PKG, + "getPkg", + [{ + name: $routeParams.name, + versionType: 'LATEST', + architectureCode: architecture.code + }] + ).then( + function(result) { + $scope.pkg = result; + $log.info('found '+result.name+' pkg'); + refreshBreadcrumbItems(); + }, + function(err) { + constants.ERRORHANDLING_JSONRPC(err,$location,$log); + } + ); + }, + function() { + $log.error('a problem has arisen getting the architecture'); + $location.path("/error").search({}); + } + + ); + } + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewuser.html b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewuser.html new file mode 100644 index 00000000..6e77d4e3 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewuser.html @@ -0,0 +1,12 @@ +
+ + + +

+ {{user.nickname}} +

+ +
+ + + diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewusercontroller.js b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewusercontroller.js new file mode 100644 index 00000000..d09112f8 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewusercontroller.js @@ -0,0 +1,52 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').controller( + 'ViewUserController', + [ + '$scope','$log','$location','$routeParams', + 'jsonRpc','constants', + function( + $scope,$log,$location,$routeParams, + jsonRpc,constants) { + + $scope.breadcrumbItems = undefined; + $scope.user = undefined; + + $scope.shouldSpin = function() { + return undefined == $scope.user; + } + + refreshUser(); + + function refreshBreadcrumbItems() { + $scope.breadcrumbItems = [{ + title : $scope.user.nickname, + path : $location.path() + }]; + } + + function refreshUser() { + jsonRpc.call( + constants.ENDPOINT_API_V1_USER, + "getUser", + [{ + nickname : $routeParams.nickname + }] + ).then( + function(result) { + $scope.user = result; + refreshBreadcrumbItems(); + $log.info('fetched user; '+result.nickname); + }, + function(err) { + constants.ERRORHANDLING_JSONRPC(err,$location,$log); + } + ); + }; + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/banner.html b/haikudepotserver-webapp/src/main/webapp/js/app/directive/banner.html new file mode 100644 index 00000000..15fb9be4 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/banner.html @@ -0,0 +1,53 @@ + + diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/bannerdirective.js b/haikudepotserver-webapp/src/main/webapp/js/app/directive/bannerdirective.js new file mode 100644 index 00000000..63955375 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/bannerdirective.js @@ -0,0 +1,117 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This directive will render the bar at the top of the screen that displays what this is and other details about + * your usage of the application; maybe language, who is logged in and so on.

+ */ + +angular.module('haikudepotserver').directive('banner',function() { + return { + restrict: 'E', + templateUrl:'/js/app/directive/banner.html', + replace: true, + controller: + [ + '$scope','$log','$location','$route', + 'userState','referenceData', + function( + $scope,$log,$location,$route, + userState,referenceData) { + + $scope.architectureOptions; + $scope.architectures; + $scope.architecture; + + referenceData.architectures().then( + function(architectures) { + $scope.architectures = architectures; + }, + function() { + $log.error('a problem has arisen obtaining the architectures'); + $location.path("/error").search({}); + } + ); + + function refreshArchitecture() { + userState.architecture().then( + function(data) { + $scope.architecture = data; + }, + function() { + $log.error('a problem has arisen obtaining the architecture'); + $location.path("/error").search({}); + } + ); + } + + refreshArchitecture(); + + $scope.isArchitectureSelected = function(a) { + return $scope.architecture && $scope.architecture.code == a.code; + } + + $scope.canCreateUser = function() { + return !userState.user() && $location.path() != '/createuser'; + } + + $scope.canAuthenticateUser = function() { + return !userState.user() && $location.path() != '/authenticateuser'; + } + + $scope.canLogoutUser = function() { + return userState.user(); + } + + $scope.goCreateUser = function() { + $location.path('/createuser').search({}); + return false; + } + + $scope.goAuthenticateUser = function() { + var p = $location.path(); + $location.path('/authenticateuser').search({ destination: p }); + return false; + } + + $scope.goLogoutUser = function() { + userState.user(null); + $location.path('/').search({}); + return false; + } + + $scope.goHome = function() { + $location.path('/').search({}); + return false; + } + + $scope.goChooseArchitecture = function(a) { + + if(!$scope.isArchitectureSelected(a)) { + + userState.architecture(a); + refreshArchitecture(); + + if($location.path() == '/') { + $route.reload(); + } + else { + $location.path('/').search({}); + } + + } + + return false; + } + + $scope.userDisplayTitle = function() { + var data = userState.user(); + return data ? data.nickname : undefined; + } + + } + ] + }; +}); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/breadcrumbs.html b/haikudepotserver-webapp/src/main/webapp/js/app/directive/breadcrumbs.html new file mode 100644 index 00000000..748f7f95 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/breadcrumbs.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/breadcrumbsdirective.js b/haikudepotserver-webapp/src/main/webapp/js/app/directive/breadcrumbsdirective.js new file mode 100644 index 00000000..0c4ce680 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/breadcrumbsdirective.js @@ -0,0 +1,71 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This directive renders a small block of HTML that represents the breadcrumbs. The breadcrumbs are not actually + * the path by which you arrived where you are, but a derived idea of where you are that is generated by the page's + * controller that you are currently looking at. Note that this directive will insert the 'home' breadcrumb item + * itself so you need not keep supplying that.

+ */ + +angular.module('haikudepotserver').directive('breadcrumbs',function() { + return { + restrict: 'E', + templateUrl:'/js/app/directive/breadcrumbs.html', + replace: true, + scope: { + items: '=' + }, + controller: + ['$scope','$location', + function($scope,$location) { + + $scope.derivedItems = undefined; + + function isItemActive(item) { + if(!item) { + return false; + } + return item.path == $location.path(); + } + + function deriveItems() { + $scope.derivedItems = [{ + title : 'Home', + path : '/' + }].concat($scope.items); + } + + deriveItems(); + + $scope.shouldShowItems = function() { + return $scope.items; + } + + $scope.$watch('items', function(newValue) { + deriveItems(); + }); + + $scope.goItem = function(item) { + $location.path(item.path).search({}); + return false; + } + + $scope.isItemActive = function(item) { + return isItemActive(item); + } + + $scope.listItemClass = function(item) { + if(isItemActive(item)) { + return [ 'active' ]; + } + + return []; + } + + } + ] + }; +}); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/errormessages.html b/haikudepotserver-webapp/src/main/webapp/js/app/directive/errormessages.html new file mode 100644 index 00000000..5a506465 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/errormessages.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/errormessagesdirective.js b/haikudepotserver-webapp/src/main/webapp/js/app/directive/errormessagesdirective.js new file mode 100644 index 00000000..7a31a3fb --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/errormessagesdirective.js @@ -0,0 +1,28 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This directive is able to be placed into error pages and allows the user to view any error messages that are + * related to the fields of the form. This means that each error message does not need to have explicit coding in + * order to display the error involved.

+ */ + +angular.module('haikudepotserver').directive('errorMessages',function() { + return { + restrict: 'E', + templateUrl:'/js/app/directive/errormessages.html', + replace: true, + scope: { + error:'=', + keyPrefix:'@' + }, + controller: + ['$scope', + function($scope) { + } + ] + }; + } +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/messagedirective.js b/haikudepotserver-webapp/src/main/webapp/js/app/directive/messagedirective.js new file mode 100644 index 00000000..c9457891 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/messagedirective.js @@ -0,0 +1,47 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This directive is able to display an error message that is provided from the application using the 'misc' API. + * The specific error message is indicated by providing a 'key' value.

+ */ + +angular.module('haikudepotserver').directive('message',function() { + return { + restrict: 'E', + template:'{{messageValue}}', + replace: true, + scope: { + key:'@' + }, + controller: + ['$scope','$log','messageSource', + function($scope,$log,messageSource) { + + $scope.messageValue = '...'; + + $scope.$watch('key',function() { + if($scope.key) { + messageSource.get($scope.key).then( + function(value) { + if(null==value) { + $log.warn('undefined message key; '+$scope.key); + $scope.messageValue=$scope.key; + } + else { + $scope.messageValue = value; + } + }, + function() { + $scope.messageValue = '???'; + }); + } + }); + + } + ] + }; + } +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/spinner.html b/haikudepotserver-webapp/src/main/webapp/js/app/directive/spinner.html new file mode 100644 index 00000000..86a99a24 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/spinner.html @@ -0,0 +1,5 @@ +
+
+ +
+
\ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/spinnerdirective.js b/haikudepotserver-webapp/src/main/webapp/js/app/directive/spinnerdirective.js new file mode 100644 index 00000000..926a36a0 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/spinnerdirective.js @@ -0,0 +1,22 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This directive will render a DIV that covers the whole page with a spinner inside it. This provides two + * functions; the first is to indicate to the user that something is going on and they ought to wait a moment. + * The second function is to cover the UI so that the user is not able to interact with the page while the + * operation is being undertaken.

+ */ + +angular.module('haikudepotserver').directive('spinner',function() { + return { + restrict: 'E', + templateUrl:'/js/app/directive/spinner.html', + replace: true, + scope: { + spin: '=' + } + }; +}); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/versionlabel.html b/haikudepotserver-webapp/src/main/webapp/js/app/directive/versionlabel.html new file mode 100644 index 00000000..58f92fef --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/versionlabel.html @@ -0,0 +1 @@ +{{version.major}}.{{version.minor}}.{{version.micro}}.{{version.preRelease}}.{{version.revision}} \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/directive/versionlabeldirective.js b/haikudepotserver-webapp/src/main/webapp/js/app/directive/versionlabeldirective.js new file mode 100644 index 00000000..a6cbd2ad --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/directive/versionlabeldirective.js @@ -0,0 +1,26 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This directive renders a version object (with the major, minor etc... values + * into a block of HTML that is supposed to be a simple and versatile block of text + * that describes the label.

+ */ + +angular.module('haikudepotserver').directive('versionLabel',function() { + return { + restrict: 'E', + templateUrl:'/js/app/directive/versionlabel.html', + replace: true, + scope: { + version: '=' + }, + controller: + ['$scope', + function($scope) { + } + ] + }; +}); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/haikudepotserver.js b/haikudepotserver-webapp/src/main/webapp/js/app/haikudepotserver.js new file mode 100644 index 00000000..7ee03ce1 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/haikudepotserver.js @@ -0,0 +1,12 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module( + 'haikudepotserver', + [ + 'ngRoute', // ability to route to different pages from the url + 'ui.bootstrap', // http://angular-ui.github.io/bootstrap/ + 'ui.directives' // https://github.com/angular-ui/ui-utils + ]); diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/routes.js b/haikudepotserver-webapp/src/main/webapp/js/app/routes.js new file mode 100644 index 00000000..f104388d --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/routes.js @@ -0,0 +1,20 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').config( + [ + '$routeProvider', + function($routeProvider) { + $routeProvider + .when('/authenticateuser',{controller:'AuthenticateUserController', templateUrl:'/js/app/controller/authenticateuser.html'}) + .when('/createuser',{controller:'CreateUserController', templateUrl:'/js/app/controller/createuser.html'}) + .when('/viewuser/:nickname',{controller:'ViewUserController', templateUrl:'/js/app/controller/viewuser.html'}) + .when('/viewpkg/:name/:version',{controller:'ViewPkgController', templateUrl:'/js/app/controller/viewpkg.html'}) + .when('/error',{controller:'ErrorController', templateUrl:'/js/app/controller/error.html'}) + .when('/',{controller:'HomeController', templateUrl:'/js/app/controller/home.html'}) + .otherwise({redirectTo:'/'}); + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/service/jsonrpcservice.js b/haikudepotserver-webapp/src/main/webapp/js/app/service/jsonrpcservice.js new file mode 100644 index 00000000..fe358a32 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/service/jsonrpcservice.js @@ -0,0 +1,154 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This service provides JSON-RPC functionality on top of the AngularJS $http service.

+ */ + +// see http://www.jsonrpc.org/specification + +angular.module('haikudepotserver').factory('jsonRpc', + [ + '$log','$http','$q', + function($log,$http,$q) { + + var JsonRpcService = { + + errorCodes : { + PARSEERROR : -32700, + INVALIDREQUEST : -32600, + METHODNOTFOUND : -32601, + INVALIDPARAMETERS : -32602, + INTERNALERROR : -32603, + TRANSPORTFAILURE : -32100, + INVALIDRESPONSE : -32101, + + VALIDATION : -32800, + OBJECTNOTFOUND : -32801, + CAPTCHABADRESPONSE : -32802 + }, + + /** + *

This is a map of HTTP headers that is sent on each JSON-RPC request into the server.

+ */ + + headers : {}, + + /** + *

This method will set the HTTP hder that is sent on each JSON-RPC request. This is handy, + * for example for authentication.

+ */ + + setHeader : function(name, value) { + + if(!name || 0==''+name.length) { + throw 'the name of the http header is required'; + } + + if(!value || 0==''+value.length) { + delete JsonRpcService.headers[name]; + } + else { + JsonRpcService.headers[name] = value; + } + + }, + + /** + *

This counter is used to generate an id which can be used to identify a request-response method + * invocation in the json-rpc potocol.

+ */ + + counter : 1000, + + /** + *

This function will call a json-rpc method on a remote system identified by the supplied endpoint. + * If no id is supplied then it will fabricate one. If there are no parameters supplied then it will + * send an empty array of parameters. This method will return a promise that is fulfilled when the + * remote server has responded.

+ */ + + call : function(endpoint, method, params, id) { + + if(!endpoint) { + throw 'the endpoint is required to invoke a json-rpc method'; + } + + if(!method) { + throw 'the method is required to invoke a json-rpc method'; + } + + if(!params) { + params = []; + } + + if(!id) { + id = JsonRpcService.counter; + JsonRpcService.counter += 1; + } + + function mkTransportErr(httpStatus) { + return mkErr(httpStatus,JsonRpcService.errorCodes.TRANSPORTFAILURE,'transport-failure'); + } + + function mkErr(httpStatus,code,message) { + return { + jsonrpc: "2.0", + id : id, + error : { + code : code, + message : message, + data : httpStatus + } + }; + } + + var deferred = $q.defer(); + + $http({ + method: 'POST', + url: endpoint, + headers: JsonRpcService.headers, + data: { + jsonrpc : "2.0", + method : method, + params : params, + id : id + }, + headers:{'Content-Type':'application/json'} + }) + .success(function(data,status,header,config) { + if(200!=status) { + deferred.reject(mkTransportErr(status)); + } + else { + if(!data.result) { + + if(!data.error) { + deferred.reject(mkErr(status,JsonRpcService.errorCodes.INVALIDRESPONSE,'invalid-response')); + } + else { + deferred.reject(data.error); + } + } + else { + deferred.resolve(data.result); + } + } + }) + .error(function(data,status,header,config) { + deferred.reject(mkTransportErr(status)); + }); + + return deferred.promise; + } + + }; + + return JsonRpcService; + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/service/messagesourceservice.js b/haikudepotserver-webapp/src/main/webapp/js/app/service/messagesourceservice.js new file mode 100644 index 00000000..e92bb1db --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/service/messagesourceservice.js @@ -0,0 +1,52 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This service obtains and stores localized messages from the application.

+ */ + +angular.module('haikudepotserver').factory('messageSource', + [ + '$log','$q','constants','jsonRpc', + function($log,$q,constants,jsonRpc) { + + var MessageSource = { + + messages : undefined, + + get : function(key) { + + var deferred = $q.defer(); + + if(MessageSource.messages) { + deferred.resolve(MessageSource.messages[key]); + } + else { + jsonRpc.call( + constants.ENDPOINT_API_V1_MISCELLANEOUS, + 'getAllMessages', + [{}] + ).then( + function(data) { + MessageSource.messages = data.messages; + deferred.resolve(MessageSource.messages[key] ? MessageSource.messages[key] : key); + }, + function(err) { + $log.warn('unable to get the messages from the server'); + deferred.reject(null); + } + ); + } + + return deferred.promise; + } + + }; + + return MessageSource; + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/service/referencedataservice.js b/haikudepotserver-webapp/src/main/webapp/js/app/service/referencedataservice.js new file mode 100644 index 00000000..87eacd4d --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/service/referencedataservice.js @@ -0,0 +1,86 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This service maintains a list of reference data objects that can be re-used in the system. This prevents the + * need to keep going back to the server to ge this material; it can be cached locally.

+ */ + +angular.module('haikudepotserver').factory('referenceData', + [ + '$log','$q','jsonRpc','constants', + function($log, $q, jsonRpc,constants) { + + var architectures = undefined; + + var ReferenceData = { + + /** + *

This function returns a promise to return the architecture identified by the code.

+ */ + + architecture : function(code) { + var deferred = $q.defer(); + + ReferenceData.architectures().then( + function(data) { + var a = _.find(data, function(item) { + return item.code == code; + }); + + if(a) { + deferred.resolve(a); + } + else { + deferred.reject(); + } + }, + function() { + deferred.reject(); + } + ) + + return deferred.promise; + }, + + /** + *

This function will return all of the architectures that are available. It will return a + * promise.

+ * @returns {*} + */ + + architectures : function() { + + var deferred = $q.defer(); + + if(architectures) { + deferred.resolve(architectures); + } + else { + jsonRpc + .call( + constants.ENDPOINT_API_V1_MISCELLANEOUS,'getAllArchitectures',[{}] + ) + .then( + function(data) { + architectures = data.architectures; + deferred.resolve(architectures); + }, + function(err) { + deferred.reject(null); + } + ); + } + + return deferred.promise; + } + + }; + + return ReferenceData; + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/app/service/userstateservice.js b/haikudepotserver-webapp/src/main/webapp/js/app/service/userstateservice.js new file mode 100644 index 00000000..131339fc --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/app/service/userstateservice.js @@ -0,0 +1,100 @@ +/* + * Copyright 2013, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + *

This service is here to maintain the current user's state. When the user logs in for example, this is stored + * here. This service may take other actions such as configuring headers in the jsonRpc service when the user logs-in + * or logs-out.

+ */ + +angular.module('haikudepotserver').factory('userState', + [ + '$log','$q','jsonRpc','referenceData','constants', + function($log, $q, jsonRpc,referenceData,constants) { + + var architecture = undefined; + var user = undefined; + + var UserState = { + + /** + *

This function will return a promise to get the current architecture that the user would like to + * see.

+ */ + + architecture : function(value) { + + if(undefined !== value) { + architecture = value; + $log.info('set architecture; '+value.code); + } + + var deferred = $q.defer(); + + if(!architecture) { + referenceData.architecture(constants.ARCHITECTURE_CODE_DEFAULT).then( + function(data) { + if(!data) { + $log.error('architecture \''+constants.ARCHITECTURE_CODE_DEFAULT+'\' unknown'); + deferred.reject(); + } + else { + architecture = data; + deferred.resolve(data); + } + }, + function() { + deferred.reject(); + } + ); + } + else { + deferred.resolve(architecture); + } + + return deferred.promise; + }, + + /** + *

Invoked with no argument, this function will return the user. If it is supplied with null then + * it will set the current user to empty. If it is supplied with a user value, it will configure the + * user.

+ */ + + user : function(value) { + if(undefined !== value) { + if(null==value) { + user = undefined; + jsonRpc.setHeader('Authorization'); // remove this header. + } + else { + + if(!value.nickname) { + throw 'the nickname is required when setting a user'; + } + + if(!value.passwordClear) { + throw 'the password clear is required when setting a user'; + } + + jsonRpc.setHeader( + 'Authorization', + 'Basic '+window.btoa(''+value.nickname+':'+value.passwordClear)); + + user = value; + $log.info('have set user; '+user.nickname); + } + } + + return user; + } + + }; + + return UserState; + + } + ] +); \ No newline at end of file diff --git a/haikudepotserver-webapp/src/main/webapp/js/lib/README.TXT b/haikudepotserver-webapp/src/main/webapp/js/lib/README.TXT new file mode 100644 index 00000000..29c3eb88 --- /dev/null +++ b/haikudepotserver-webapp/src/main/webapp/js/lib/README.TXT @@ -0,0 +1,4 @@ +This directory contains JavaScript libraries which are used for the Haiku Depot Server's web user interface. These +libraries are downloaded when the first build is done from the command line; use "mvn compile" from the command line +at the reactor level to make this happen. The actual process occurs using an ant script that is invoked from the +maven build. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..05036d00 --- /dev/null +++ b/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + org.haikuos + haikudepotserver + pom + 1.0.1-SNAPSHOT + + + haikudepotserver-webapp + haikudepotserver-api1 + haikudepotserver-packagefile + haikudepotserver-parent + + +