JUNEAU-140 Provide initial contents of PetStore modules.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PageArgs.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PageArgs.java
new file mode 100644
index 0000000..2487098
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PageArgs.java
@@ -0,0 +1,37 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.pojotools;
+
+/**
+ * TODO
+ */
+public class PageArgs {
+
+ /**
+ * TODO
+ *
+ * @return TODO
+ */
+ public int getLimit() {
+ return 0;
+ }
+
+ /**
+ * TODO
+ *
+ * @return TODO
+ */
+ public int getPosition() {
+ return 0;
+ }
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-api/src/main/java/org/apache/juneau/petstore/PetStore.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-api/src/main/java/org/apache/juneau/petstore/PetStore.java
new file mode 100644
index 0000000..ef2add7
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-api/src/main/java/org/apache/juneau/petstore/PetStore.java
@@ -0,0 +1,421 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore;
+
+import static org.apache.juneau.http.HttpMethodName.*;
+
+import java.util.*;
+
+import org.apache.juneau.jsonschema.annotation.Items;
+import org.apache.juneau.petstore.dto.*;
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.remote.*;
+import org.apache.juneau.http.exception.*;
+import org.apache.juneau.http.response.*;
+
+/**
+ * Defines the interface for both the server-side and client-side pet store application.
+ *
+ * <p>
+ * On the server side, this interface is implemented by the <c>PetStoreResource</c> class.
+ *
+ * <p>
+ * On the client side, this interface is instantiated as a proxy using the <c>RestClient.getRemoteProxy()</c> method.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+@RemoteResource(path="/petstore")
+public interface PetStore {
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Pets
+ //------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Returns all pets in the database.
+ *
+ * @return All pets in the database.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/pet")
+ public Collection<Pet> getPets() throws NotAcceptable;
+
+ /**
+ * Returns a pet from the database.
+ *
+ * @param petId The ID of the pet to retrieve.
+ * @return The pet.
+ * @throws IdNotFound Pet was not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(path="/pet/{petId}") /* method inferred from method name */
+ public Pet getPet(
+ @Path(
+ name="petId",
+ description="ID of pet to return",
+ example="123"
+ )
+ long petId
+ ) throws IdNotFound, NotAcceptable;
+
+ /**
+ * Adds a pet to the database.
+ *
+ * @param pet The pet data to add to the database.
+ * @return {@link Ok} if successful.
+ * @throws IdConflict ID already in use.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ * @throws UnsupportedMediaType Unsupported <c>Content-Type</c> header specified.
+ */
+ @RemoteMethod /* method and path inferred from method name */
+ public long postPet(
+ @Body(
+ description="Pet object to add to the store"
+ ) CreatePet pet
+ ) throws IdConflict, NotAcceptable, UnsupportedMediaType;
+
+ /**
+ * Updates a pet in the database.
+ *
+ * @param pet The pet data to add to the database.
+ * @return {@link Ok} if successful.
+ * @throws IdNotFound ID not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ * @throws UnsupportedMediaType Unsupported <c>Content-Type</c> header specified.
+ */
+ @RemoteMethod(method=PUT, path="/pet/{petId}")
+ public Ok updatePet(
+ @Body(
+ description="Pet object that needs to be added to the store"
+ ) UpdatePet pet
+ ) throws IdNotFound, NotAcceptable, UnsupportedMediaType;
+
+ /**
+ * Find all pets with the matching statuses.
+ *
+ * @param status The statuses to match against.
+ * @return The pets that match the specified statuses.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/pet/findByStatus")
+ public Collection<Pet> findPetsByStatus(
+ @Query(
+ name="status",
+ description="Status values that need to be considered for filter.",
+ required=true,
+ type="array",
+ collectionFormat="csv",
+ items=@Items(
+ type="string",
+ _enum="AVAILABLE,PENDING,SOLD",
+ _default="AVAILABLE"
+ ),
+ example="AVALIABLE,PENDING"
+ )
+ PetStatus[] status
+ ) throws NotAcceptable;
+
+ /**
+ * Find all pets with the specified tags.
+ *
+ * @param tags The tags to match against.
+ * @return The pets that match the specified tags.
+ * @throws InvalidTag Invalid tag was specified.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/pet/findByTags")
+ @Deprecated
+ public Collection<Pet> findPetsByTags(
+ @Query(
+ name="tags",
+ description="Tags to filter by",
+ required=true,
+ example="['tag1','tag2']"
+ )
+ String[] tags
+ ) throws InvalidTag, NotAcceptable;
+
+ /**
+ * Deletes the specified pet.
+ *
+ * @param apiKey Security key.
+ * @param petId ID of pet to delete.
+ * @return {@link Ok} if successful.
+ * @throws IdNotFound Pet not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=DELETE, path="/pet/{petId}")
+ public Ok deletePet(
+ @Header(
+ name="api_key",
+ description="Security API key",
+ required=true,
+ example="foobar"
+ )
+ String apiKey,
+ @Path(
+ name="petId",
+ description="Pet id to delete",
+ example="123"
+ )
+ long petId
+ ) throws IdNotFound, NotAcceptable;
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Orders
+ //------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Returns all orders in the database.
+ *
+ * @return All orders in the database.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/store/order")
+ public Collection<Order> getOrders() throws NotAcceptable;
+
+ /**
+ * Returns an order from the database.
+ *
+ * @param orderId The ID of the order to retreieve.
+ * @return The retrieved order.
+ * @throws InvalidId ID was invalid.
+ * @throws IdNotFound Order was not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/store/order/{orderId}")
+ public Order getOrder(
+ @Path(
+ name="orderId",
+ description="ID of order to fetch",
+ maximum="1000",
+ minimum="1",
+ example="123"
+ )
+ long orderId
+ ) throws InvalidId, IdNotFound, NotAcceptable;
+
+ /**
+ * Adds an order to the database.
+ *
+ * @param petId Id of pet to order.
+ * @param username The username of the user placing the order.
+ * @return The ID of the order.
+ * @throws IdConflict ID was already in use.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ * @throws UnsupportedMediaType Unsupported <c>Content-Type</c> header specified.
+ */
+ @RemoteMethod(method=POST, path="/store/order")
+ public long placeOrder(
+ @FormData(
+ name="petId",
+ description="Pet ID"
+ )
+ long petId,
+ @FormData(
+ name="username",
+ description="The username of the user creating the order"
+ )
+ String username
+ ) throws IdConflict, NotAcceptable, UnsupportedMediaType;
+
+ /**
+ * Deletes an order from the database.
+ *
+ * @param orderId The order ID.
+ * @return {@link Ok} if successful.
+ * @throws InvalidId ID not valid.
+ * @throws IdNotFound Order not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=DELETE, path="/store/order/{orderId}")
+ public Ok deleteOrder(
+ @Path(
+ name="orderId",
+ description="ID of the order that needs to be deleted",
+ minimum="1",
+ example="5"
+ )
+ long orderId
+ ) throws InvalidId, IdNotFound, NotAcceptable;
+
+ /**
+ * Returns an inventory of pet statuses and counts.
+ *
+ * @return An inventory of pet statuses and counts.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/store/inventory")
+ public Map<PetStatus,Integer> getStoreInventory() throws NotAcceptable;
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Users
+ //------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Returns all users in the database.
+ *
+ * @return All users in the database.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/user")
+ public Collection<User> getUsers() throws NotAcceptable;
+
+ /**
+ * Returns a user from the database.
+ *
+ * @param username The username.
+ * @return The user.
+ * @throws InvalidUsername Invalid username.
+ * @throws IdNotFound username not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/user/{username}")
+ public User getUser(
+ @Path(
+ name="username",
+ description="The name that needs to be fetched. Use user1 for testing."
+ )
+ String username
+ ) throws InvalidUsername, IdNotFound, NotAcceptable;
+
+ /**
+ * Adds a new user to the database.
+ *
+ * @param user The user to add to the database.
+ * @return {@link Ok} if successful.
+ * @throws InvalidUsername Username was invalid.
+ * @throws IdConflict Username already in use.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ * @throws UnsupportedMediaType Unsupported <c>Content-Type</c> header specified.
+ */
+ @RemoteMethod
+ public Ok postUser(
+ @Body(
+ description="Created user object"
+ )
+ User user
+ ) throws InvalidUsername, IdConflict, NotAcceptable, UnsupportedMediaType;
+
+ /**
+ * Bulk creates users.
+ *
+ * @param users The users to add to the database.
+ * @return {@link Ok} if successful.
+ * @throws InvalidUsername Username was invalid.
+ * @throws IdConflict Username already in use.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ * @throws UnsupportedMediaType Unsupported <c>Content-Type</c> header specified.
+ */
+ @RemoteMethod(method=POST, path="/user/createWithArray")
+ public Ok createUsers(
+ @Body(
+ description="List of user objects"
+ )
+ User[] users
+ ) throws InvalidUsername, IdConflict, NotAcceptable, UnsupportedMediaType;
+
+ /**
+ * Updates a user in the database.
+ *
+ * @param username The username.
+ * @param user The updated information.
+ * @return {@link Ok} if successful.
+ * @throws InvalidUsername Username was invalid.
+ * @throws IdNotFound User was not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ * @throws UnsupportedMediaType Unsupported <c>Content-Type</c> header specified.
+ */
+ @RemoteMethod(method=PUT, path="/user/{username}")
+ public Ok updateUser(
+ @Path(
+ name="username",
+ description="Name that need to be updated"
+ )
+ String username,
+ @Body(
+ description="Updated user object"
+ )
+ User user
+ ) throws InvalidUsername, IdNotFound, NotAcceptable, UnsupportedMediaType;
+
+ /**
+ * Deletes a user from the database.
+ *
+ * @param username The username.
+ * @return {@link Ok} if successful.
+ * @throws InvalidUsername Username was not valid.
+ * @throws IdNotFound User was not found.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=DELETE, path="/user/{username}")
+ public Ok deleteUser(
+ @Path(
+ name="username",
+ description="The name that needs to be deleted"
+ )
+ String username
+ ) throws InvalidUsername, IdNotFound, NotAcceptable;
+
+ /**
+ * User login.
+ *
+ * @param username The username for login.
+ * @param password The password for login in clear text.
+ * @param rateLimit Calls per hour allowed by the user.
+ * @param expiresAfter The <bc>Expires-After</bc> response header.
+ * @param req The servlet request.
+ * @param res The servlet response.
+ * @return {@link Ok} if successful.
+ * @throws InvalidLogin Login was unsuccessful.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/user/login")
+ public Ok login(
+ @Query(
+ name="username",
+ description="The username for login.",
+ required=true,
+ example="myuser"
+ )
+ String username,
+ @Query(
+ name="password",
+ description="The password for login in clear text.",
+ required=true,
+ example="abc123"
+ )
+ String password,
+ @ResponseHeader(
+ name="X-Rate-Limit",
+ type="integer",
+ format="int32",
+ description="Calls per hour allowed by the user.",
+ example="123"
+ )
+ Value<Integer> rateLimit,
+ Value<ExpiresAfter> expiresAfter
+ ) throws InvalidLogin, NotAcceptable;
+
+ /**
+ * User logout.
+ *
+ * @return {@link Ok} if successful.
+ * @throws NotAcceptable Unsupported <c>Accept</c> header specified.
+ */
+ @RemoteMethod(method=GET, path="/user/logout")
+ public Ok logout() throws NotAcceptable;
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-client/pom.xml b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-client/pom.xml
index ee7f019..dd5aa42 100644
--- a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-client/pom.xml
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-client/pom.xml
@@ -43,6 +43,7 @@
<dependencies>
+
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-examples-petstore-api</artifactId>
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-client/src/main/java/org/apache/juneau/petstore/Main.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-client/src/main/java/org/apache/juneau/petstore/Main.java
new file mode 100644
index 0000000..ecc9c4e
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-client/src/main/java/org/apache/juneau/petstore/Main.java
@@ -0,0 +1,105 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore;
+
+import static java.text.MessageFormat.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.json.*;
+import org.apache.juneau.marshall.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.petstore.dto.*;
+import org.apache.juneau.rest.client.*;
+
+/**
+ * Example code showing how to connect to the PetStore application using a remote proxy.
+ *
+ * <p>
+ * The remote proxy allows you to make REST calls against our REST interface through Java interface method calls.
+ */
+public class Main {
+
+ private static final JsonParser JSON_PARSER = JsonParser.create().ignoreUnknownBeanProperties().build();
+
+ public static void main(String[] args) {
+
+ // Create a RestClient with JSON serialization support.
+ try (RestClient rc = RestClient.create(SimpleJsonSerializer.class, JsonParser.class).build()) {
+
+ // Instantiate our proxy.
+ PetStore petStore = rc.getRemoteResource(PetStore.class, "http://localhost:5000");
+
+ // Print out the pets in the store.
+ Collection<Pet> pets = petStore.getPets();
+
+ // Pretty-print them to SYSOUT.
+ SimpleJson.DEFAULT_READABLE.println(pets);
+
+ // Initialize the application through REST calls.
+ init(new PrintWriter(System.err), petStore);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Initialize the petstore database by using a remote resource interface against our REST.
+ *
+ * @param w Console output.
+ * @return This object (for method chaining).
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException Thrown by client stream.
+ */
+ public static void init(PrintWriter w, PetStore ps) throws ParseException, IOException {
+
+ for (Pet x : ps.getPets()) {
+ ps.deletePet("apiKey", x.getId());
+ w.println(format("Deleted pet: id={0}", x.getId()));
+ }
+ for (Order x : ps.getOrders()) {
+ ps.deleteOrder(x.getId());
+ w.println(format("Deleted order: id={0}", x.getId()));
+ }
+ for (User x : ps.getUsers()) {
+ ps.deleteUser(x.getUsername());
+ w.println(format("Deleted user: username={0}", x.getUsername()));
+ }
+ for (CreatePet x : load("init/Pets.json", CreatePet[].class)) {
+ long id = ps.postPet(x);
+ w.println(format("Created pet: id={0}, name={1}", id, x.getName()));
+ }
+ for (Order x : load("init/Orders.json", Order[].class)) {
+ long id = ps.placeOrder(x.getPetId(), x.getUsername());
+ w.println(format("Created order: id={0}", id));
+ }
+ for (User x : load("init/Users.json", User[].class)) {
+ ps.postUser(x);
+ w.println(format("Created user: username={0}", x.getUsername()));
+ }
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Helper methods
+ //-----------------------------------------------------------------------------------------------------------------
+
+ private static <T> T load(String fileName, Class<T> c) throws ParseException, IOException {
+ return JSON_PARSER.parse(getStream(fileName), c);
+ }
+
+ private static InputStream getStream(String fileName) {
+ return Main.class.getResourceAsStream(fileName);
+ }
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/pom.xml b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/pom.xml
index 5f70636..504140d 100644
--- a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/pom.xml
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/pom.xml
@@ -42,6 +42,37 @@
<dependencies>
+ <!-- Juneau dependencies -->
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-examples-petstore-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-server-springboot</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <!-- Spring Boot dependencies-->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ <version>${springframework.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-logging</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- Needed for @Inject support -->
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <version>${javax.inject.version}</version>
+ </dependency>
</dependencies>
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/App.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/App.java
new file mode 100644
index 0000000..062b75d
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/App.java
@@ -0,0 +1,34 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore;
+
+import org.apache.juneau.rest.springboot.JuneauRestInitializer;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.context.ConfigurableApplicationContext;
+
+/**
+ * Entry point for PetStore application.
+ */
+@SpringBootApplication
+public class App {
+
+ public static void main(String[] args) {
+ new App().start(args);
+ }
+
+ protected void start(String[] args) {
+ ConfigurableApplicationContext context = new SpringApplicationBuilder(App.class).initializers(new JuneauRestInitializer(App.class)).run(args);
+ AppConfiguration.setAppContext(context);
+ }
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/AppConfiguration.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/AppConfiguration.java
new file mode 100644
index 0000000..462b274
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/AppConfiguration.java
@@ -0,0 +1,56 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore;
+
+import org.apache.juneau.petstore.rest.*;
+import org.apache.juneau.rest.springboot.annotation.JuneauRestRoot;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.*;
+
+@Configuration
+public class AppConfiguration {
+
+ public static String DEFAULT_JDBC_URL = "jdbc:h2:mem:testdb;MODE=PostgreSQL";
+ public static String DEFAULT_JDBC_USERNAME = "sa";
+ public static String DEFAULT_JDBC_PASSWORD = "";
+
+ @Autowired
+ private static volatile ApplicationContext appContext;
+
+ public static ApplicationContext getAppContext() {
+ return appContext;
+ }
+
+ public static void setAppContext(ApplicationContext appContext) {
+ AppConfiguration.appContext = appContext;
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Services
+ //-----------------------------------------------------------------------------------------------------------------
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // REST
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @Bean @JuneauRestRoot
+ public RootResources rootResources() {
+ return new RootResources();
+ }
+
+ @Bean
+ public PetStoreResource petStoreResource() {
+ return new PetStoreResource();
+ }
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/PetStoreResource.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/PetStoreResource.java
new file mode 100644
index 0000000..49807b3
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/PetStoreResource.java
@@ -0,0 +1,685 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the 'License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore.rest;
+
+import static org.apache.juneau.dto.html5.HtmlBuilder.*;
+import static org.apache.juneau.dto.swagger.ui.SwaggerUI.*;
+import static org.apache.juneau.http.HttpMethodName.*;
+import static org.apache.juneau.http.response.Ok.*;
+
+import java.util.*;
+import java.util.Map;
+
+import javax.inject.*;
+
+import org.apache.juneau.jsonschema.annotation.*;
+import org.apache.juneau.petstore.*;
+import org.apache.juneau.petstore.dto.*;
+import org.apache.juneau.petstore.service.*;
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.dto.html5.*;
+import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.http.exception.*;
+import org.apache.juneau.rest.helper.*;
+import org.apache.juneau.http.response.*;
+import org.apache.juneau.rest.widget.*;
+import org.apache.juneau.transforms.*;
+import org.apache.juneau.rest.converters.*;
+
+/**
+ * Sample Petstore application.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+@RestResource(
+ path="/petstore",
+ title="Petstore application",
+ description={
+ "This is a sample server Petstore server based on the Petstore sample at Swagger.io.",
+ "You can find out more about Swagger at http://swagger.io.",
+ },
+ properties= {
+ // Resolve recursive references when showing schema info in the swagger.
+ @Property(name=SWAGGERUI_resolveRefsMaxDepth, value="99")
+ },
+ swagger=@ResourceSwagger(
+ version="1.0.0",
+ title="Swagger Petstore",
+ termsOfService="You are on your own.",
+ contact=@Contact(
+ name="Juneau Development Team",
+ email="dev@juneau.apache.org",
+ url="http://juneau.apache.org"
+ ),
+ license=@License(
+ name="Apache 2.0",
+ url="http://www.apache.org/licenses/LICENSE-2.0.html"
+ ),
+ externalDocs=@ExternalDocs(
+ description="Find out more about Juneau",
+ url="http://juneau.apache.org"
+ ),
+ tags={
+ @Tag(
+ name="pet",
+ description="Everything about your Pets",
+ externalDocs=@ExternalDocs(
+ description="Find out more",
+ url="http://juneau.apache.org"
+ )
+ ),
+ @Tag(
+ name="store",
+ description="Access to Petstore orders"
+ ),
+ @Tag(
+ name="user",
+ description="Operations about user",
+ externalDocs=@ExternalDocs(
+ description="Find out more about our store",
+ url="http://juneau.apache.org"
+ )
+ )
+ }
+ ),
+ staticFiles={"htdocs:htdocs"}, // Expose static files in htdocs subpackage.
+ children={
+ SqlQueryResource.class,
+ PhotosResource.class
+ }
+)
+@HtmlDocConfig(
+ widgets={
+ ContentTypeMenuItem.class,
+ ThemeMenuItem.class,
+ },
+ navlinks={
+ "up: request:/..",
+ "options: servlet:/?method=OPTIONS",
+ "init: servlet:/init",
+ "$W{ContentTypeMenuItem}",
+ "$W{ThemeMenuItem}",
+ "source: $C{Source/gitHub}/org/apache/juneau/examples/rest/petstore/$R{servletClassSimple}.java"
+ },
+ head={
+ "<link rel='icon' href='$U{servlet:/htdocs/cat.png}'/>" // Add a cat icon to the page.
+ },
+ header={
+ "<h1>$R{resourceTitle}</h1>",
+ "<h2>$R{methodSummary}</h2>",
+ "$C{PetStore/headerImage}"
+ },
+ aside={
+ "<div style='max-width:400px' class='text'>",
+ " <p>This page shows a standard nested REST resource.</p>",
+ " <p>It shows how different properties can be rendered on the same bean in different views.</p>",
+ " <p>It also shows examples of HtmlRender classes and @BeanProperty(format) annotations.</p>",
+ " <p>It also shows how the Queryable converter and query widget can be used to create searchable interfaces.</p>",
+ "</div>"
+ },
+ stylesheet="servlet:/htdocs/themes/dark.css" // Use dark theme by default.
+)
+public class PetStoreResource extends BasicRest implements PetStore {
+
+ @Inject
+ private PetStoreService store;
+
+ /**
+ * Navigation page
+ *
+ * @return Navigation page contents.
+ */
+ @RestMethod(
+ name=GET,
+ path="/",
+ summary="Navigation page"
+ )
+ @HtmlDocConfig(
+ style={
+ "INHERIT", // Flag for inheriting resource-level CSS.
+ "body { ",
+ "background-image: url('petstore/htdocs/background.jpg'); ",
+ "background-color: black; ",
+ "background-size: cover; ",
+ "background-attachment: fixed; ",
+ "}"
+ }
+ )
+ public ResourceDescriptions getTopPage() {
+ return new ResourceDescriptions()
+ .append("pet", "All pets in the store")
+ .append("store", "Orders and inventory")
+ .append("user", "Petstore users")
+ .append("photos", "Photos service")
+ .append("sql", "SQL query service")
+ ;
+ }
+
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ // Initialization
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Initialize database form entry page.
+ *
+ * @return Initialize database form entry page contents.
+ */
+ @RestMethod(
+ summary="Initialize database form entry page"
+ )
+ public Div getInit() {
+ return div(
+ form("servlet:/init").method(POST).target("buf").children(
+ table(
+ tr(
+ th("Initialize petstore database:"),
+ td(input("radio").name("init-method").value("direct").checked(true), "direct", input("radio").name("init-method").value("rest"), "rest"),
+ td(button("submit", "Submit").style("float:right").onclick("scrolling=true"))
+ )
+ )
+ ),
+ br(),
+ iframe().id("buf").name("buf").style("width:800px;height:600px;").onload("window.parent.scrolling=false;"),
+ script("text/javascript",
+ "var scrolling = false;",
+ "function scroll() { if (scrolling) { document.getElementById('buf').contentWindow.scrollBy(0,50); } setTimeout('scroll()',200); } ",
+ "scroll();"
+ )
+ );
+ }
+
+ /**
+ * Initialize database.
+ *
+ * @param initMethod The method used to initialize the database.
+ * @param res HTTP request.
+ * @throws Exception Error occurred.
+ */
+ @RestMethod(
+ summary="Initialize database"
+ )
+ public void postInit(
+ RestResponse res
+ ) throws Exception {
+ res.setHeader("Content-Encoding", "identity");
+ store.initDirect(res.getDirectWriter("text/plain"));
+ }
+
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ // Pets
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+ @Override /* PetStore */
+ @RestMethod(
+ name=GET,
+ path="/pet",
+ summary="All pets in the store",
+ swagger=@MethodSwagger(
+ tags="pet",
+ parameters={
+ Queryable.SWAGGER_PARAMS
+ }
+ ),
+ converters={Queryable.class}
+ )
+ @BeanConfig(
+ bpx="Pet: tags,photo"
+ )
+ public Collection<Pet> getPets() throws NotAcceptable {
+ return store.getPets();
+ }
+
+ @Override /* PetStore */
+ @RestMethod(
+ name=GET,
+ path="/pet/{petId}",
+ summary="Find pet by ID",
+ description="Returns a single pet",
+ swagger=@MethodSwagger(
+ tags="pet",
+ value={
+ "security:[ { api_key:[] } ]"
+ }
+ )
+ )
+ public Pet getPet(long petId) throws IdNotFound, NotAcceptable {
+ return store.getPet(petId);
+ }
+
+ @Override /* PetStore */
+ @RestMethod(
+ summary="Add a new pet to the store",
+ swagger=@MethodSwagger(
+ tags="pet",
+ value={
+ "security:[ { petstore_auth:['write:pets','read:pets'] } ]"
+ }
+ )
+ )
+ public long postPet(CreatePet pet) throws IdConflict, NotAcceptable, UnsupportedMediaType {
+ return store.create(pet).getId();
+ }
+
+ @Override /* PetStore */
+ @RestMethod(
+ name=PUT,
+ path="/pet/{petId}",
+ summary="Update an existing pet",
+ swagger=@MethodSwagger(
+ tags="pet",
+ value={
+ "security:[ { petstore_auth: ['write:pets','read:pets'] } ]"
+ }
+ )
+ )
+ public Ok updatePet(UpdatePet pet) throws IdNotFound, NotAcceptable, UnsupportedMediaType {
+ store.update(pet);
+ return OK;
+ }
+
+ @Override /* PetStore */
+ @RestMethod(
+ name=GET,
+ path="/pet/findByStatus",
+ summary="Finds Pets by status",
+ description="Multiple status values can be provided with comma separated strings.",
+ swagger=@MethodSwagger(
+ tags="pet",
+ value={
+ "security:[{petstore_auth:['write:pets','read:pets']}]"
+ }
+ )
+ )
+ public Collection<Pet> findPetsByStatus(PetStatus[] status) throws NotAcceptable {
+ return store.getPetsByStatus(status);
+ }
+
+ @Override /* PetStore */
+ @RestMethod(
+ name=GET,
+ path="/pet/findByTags",
+ summary="Finds Pets by tags",
+ description="Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+ swagger=@MethodSwagger(
+ tags="pet",
+ value={
+ "security:[ { petstore_auth:[ 'write:pets','read:pets' ] } ]"
+ }
+ )
+ )
+ @Deprecated
+ public Collection<Pet> findPetsByTags(String[] tags) throws InvalidTag, NotAcceptable {
+ return store.getPetsByTags(tags);
+ }
+
+ @Override /* PetStore */
+ @RestMethod(
+ name=DELETE,
+ path="/pet/{petId}",
+ summary="Deletes a pet",
+ swagger=@MethodSwagger(
+ tags="pet",
+ value={
+ "security:[ { petstore_auth:[ 'write:pets','read:pets' ] } ]"
+ }
+ )
+ )
+ public Ok deletePet(String apiKey, long petId) throws IdNotFound, NotAcceptable {
+ store.removePet(petId);
+ return OK;
+ }
+
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ // Pets - extra methods
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Displays the pet edit page.
+ *
+ * @param petId ID of pet to edit
+ * @return Edit page contents.
+ * @throws NotAcceptable Unsupported <bc>Accept</bc> header value specified.
+ * @throws UnsupportedMediaType Unsupported <bc>Content-Type</bc> header value specified.
+ */
+ @RestMethod(
+ name=GET,
+ path="/pet/{petId}/edit",
+ summary="Pet edit page",
+ swagger=@MethodSwagger(
+ tags="pet",
+ value={
+ "security:[ { petstore_auth:['write:pets','read:pets'] } ]"
+ }
+ )
+ )
+ public Div editPetPage(
+ @Path(
+ name="petId",
+ description="ID of pet to edit",
+ example="123"
+ )
+ long petId
+ ) throws NotAcceptable, UnsupportedMediaType {
+
+ Pet pet = getPet(petId);
+
+ return div(
+ form().id("form").action("servlet:/pet/" + petId).method(POST).children(
+ table(
+ tr(
+ th("Id:"),
+ td(input().name("id").type("text").value(petId).readonly(true)),
+ td(new Tooltip("❓", "The name of the pet.", br(), "e.g. 'Fluffy'"))
+ ),
+ tr(
+ th("Name:"),
+ td(input().name("name").type("text").value(pet.getName())),
+ td(new Tooltip("❓", "The name of the pet.", br(), "e.g. 'Fluffy'"))
+ ),
+ tr(
+ th("Species:"),
+ td(
+ select().name("species").children(
+ option("cat"), option("dog"), option("bird"), option("fish"), option("mouse"), option("rabbit"), option("snake")
+ ).choose(pet.getSpecies())
+ ),
+ td(new Tooltip("❓", "The kind of animal."))
+ ),
+ tr(
+ th("Price:"),
+ td(input().name("price").type("number").placeholder("1.0").step("0.01").min(1).max(100).value(pet.getPrice())),
+ td(new Tooltip("❓", "The price to charge for this pet."))
+ ),
+ tr(
+ th("Tags:"),
+ td(input().name("tags").type("text").value(StringUtils.join(pet.getTags(), ','))),
+ td(new Tooltip("❓", "Arbitrary textual tags (comma-delimited).", br(), "e.g. 'fluffy,friendly'"))
+ ),
+ tr(
+ th("Status:"),
+ td(
+ select().name("status").children(
+ option("AVAILABLE"), option("PENDING"), option("SOLD")
+ ).choose(pet.getStatus())
+ ),
+ td(new Tooltip("❓", "The current status of the animal."))
+ ),
+ tr(
+ td().colspan(2).style("text-align:right").children(
+ button("reset", "Reset"),
+ button("button","Cancel").onclick("window.location.href='/'"),
+ button("submit", "Submit")
+ )
+ )
+ ).style("white-space:nowrap")
+ )
+ );
+ }
+
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ // Orders
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Store navigation page.
+ *
+ * @return Store navigation page contents.
+ */
+ @RestMethod(
+ summary="Store navigation page",
+ swagger=@MethodSwagger(
+ tags="store"
+ )
+ )
+ public ResourceDescriptions getStore() {
+ return new ResourceDescriptions()
+ .append("store/order", "Petstore orders")
+ .append("store/inventory", "Petstore inventory")
+ ;
+ }
+
+ @Override
+ @RestMethod(
+ name=GET,
+ path="/store/order",
+ summary="Petstore orders",
+ swagger=@MethodSwagger(
+ tags="store"
+ )
+ )
+ @HtmlDocConfig(
+ widgets={
+ QueryMenuItem.class
+ },
+ navlinks={
+ "INHERIT", // Inherit links from class.
+ "[2]:$W{QueryMenuItem}", // Insert QUERY link in position 2.
+ "[3]:$W{AddOrderMenuItem}" // Insert ADD link in position 3.
+ }
+ )
+ public Collection<Order> getOrders() throws NotAcceptable {
+ return store.getOrders();
+ }
+
+ @Override
+ @RestMethod(
+ name=GET,
+ path="/store/order/{orderId}",
+ summary="Find purchase order by ID",
+ description="Returns a purchase order by ID.",
+ swagger=@MethodSwagger(
+ tags="store"
+ )
+ )
+ public Order getOrder(long orderId) throws InvalidId, IdNotFound, NotAcceptable {
+ if (orderId < 1 || orderId > 1000)
+ throw new InvalidId();
+ return store.getOrder(orderId);
+ }
+
+ @Override
+ @RestMethod(
+ name=POST,
+ path="/store/order",
+ summary="Place an order for a pet",
+ swagger=@MethodSwagger(
+ tags="store"
+ ),
+ pojoSwaps={
+ TemporalDateSwap.IsoLocalDate.class
+ }
+ )
+ public long placeOrder(long petId, String username) throws IdConflict, NotAcceptable, UnsupportedMediaType {
+ CreateOrder co = new CreateOrder(petId, username);
+ return store.create(co).getId();
+ }
+
+ @Override
+ @RestMethod(
+ name=DELETE,
+ path="/store/order/{orderId}",
+ summary="Delete purchase order by ID",
+ description= {
+ "For valid response try integer IDs with positive integer value.",
+ "Negative or non-integer values will generate API errors."
+ },
+ swagger=@MethodSwagger(
+ tags="store"
+ )
+ )
+ public Ok deleteOrder(long orderId) throws InvalidId, IdNotFound, NotAcceptable {
+ if (orderId < 0)
+ throw new InvalidId();
+ store.removeOrder(orderId);
+ return OK;
+ }
+
+ @Override
+ @RestMethod(
+ name=GET,
+ path="/store/inventory",
+ summary="Returns pet inventories by status",
+ description="Returns a map of status codes to quantities",
+ swagger=@MethodSwagger(
+ tags="store",
+ responses={
+ "200:{ 'x-example':{AVAILABLE:123} }",
+ },
+ value={
+ "security:[ { api_key:[] } ]"
+ }
+ )
+ )
+ public Map<PetStatus,Integer> getStoreInventory() throws NotAcceptable {
+ return store.getInventory();
+ }
+
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ // Users
+ //-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+ @Override
+ @RestMethod(
+ name=GET,
+ path="/user",
+ summary="Petstore users",
+ bpx="User: email,password,phone",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public Collection<User> getUsers() throws NotAcceptable {
+ return store.getUsers();
+ }
+
+ @Override
+ @RestMethod(
+ name=GET,
+ path="/user/{username}",
+ summary="Get user by user name",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public User getUser(String username) throws InvalidUsername, IdNotFound, NotAcceptable {
+ return store.getUser(username);
+ }
+
+ @Override
+ @RestMethod(
+ summary="Create user",
+ description="This can only be done by the logged in user.",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public Ok postUser(User user) throws InvalidUsername, IdConflict, NotAcceptable, UnsupportedMediaType {
+ store.create(user);
+ return OK;
+ }
+
+ @Override
+ @RestMethod(
+ name=POST,
+ path="/user/createWithArray",
+ summary="Creates list of users with given input array",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public Ok createUsers(User[] users) throws InvalidUsername, IdConflict, NotAcceptable, UnsupportedMediaType {
+ for (User user : users)
+ store.create(user);
+ return OK;
+ }
+
+ @Override
+ @RestMethod(
+ name=PUT,
+ path="/user/{username}",
+ summary="Update user",
+ description="This can only be done by the logged in user.",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public Ok updateUser(String username, User user) throws InvalidUsername, IdNotFound, NotAcceptable, UnsupportedMediaType {
+ store.update(user);
+ return OK;
+ }
+
+ @Override
+ @RestMethod(
+ name=DELETE,
+ path="/user/{username}",
+ summary="Delete user",
+ description="This can only be done by the logged in user.",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public Ok deleteUser(String username) throws InvalidUsername, IdNotFound, NotAcceptable {
+ store.removeUser(username);
+ return OK;
+ }
+
+ @Override
+ @RestMethod(
+ name=GET,
+ path="/user/login",
+ summary="Logs user into the system",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public Ok login(
+ String username,
+ String password,
+ Value<Integer> rateLimit,
+ Value<ExpiresAfter> expiresAfter
+ ) throws InvalidLogin, NotAcceptable {
+
+ RestRequest req = getRequest();
+
+ if (! store.isValid(username, password))
+ throw new InvalidLogin();
+
+ Date d = new Date(System.currentTimeMillis() + 30 * 60 * 1000);
+ req.getSession().setAttribute("login-expires", d);
+ rateLimit.set(1000);
+ expiresAfter.set(new ExpiresAfter(d));
+ return OK;
+ }
+
+ @Override
+ @RestMethod(
+ name=GET,
+ path="/user/logout",
+ summary="Logs out current logged in user session",
+ swagger=@MethodSwagger(
+ tags="user"
+ )
+ )
+ public Ok logout() throws NotAcceptable {
+ getRequest().getSession().removeAttribute("login-expires");
+ return OK;
+ }
+}
\ No newline at end of file
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/PhotosResource.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/PhotosResource.java
new file mode 100644
index 0000000..1eeb058
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/PhotosResource.java
@@ -0,0 +1,327 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore.rest;
+
+import static org.apache.juneau.http.HttpMethodName.*;
+
+import org.apache.juneau.jsonschema.annotation.ExternalDocs;
+import org.apache.juneau.jsonschema.annotation.Schema;
+import java.awt.image.*;
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.Map;
+
+import javax.imageio.*;
+import javax.servlet.*;
+import javax.servlet.http.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.annotation.Body;
+import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.http.annotation.Response;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.http.exception.*;
+import org.apache.juneau.rest.helper.*;
+import org.apache.juneau.rest.matchers.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Sample resource that allows images to be uploaded and retrieved.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+@RestResource(
+ path="/photos",
+ messages="nls/PhotosResource",
+ title="Photo REST service",
+ description="Sample resource that allows images to be uploaded and retrieved.",
+ swagger=@ResourceSwagger(
+ contact=@Contact(name="Juneau Developer",email="dev@juneau.apache.org"),
+ license=@License(name="Apache 2.0",url="http://www.apache.org/licenses/LICENSE-2.0.html"),
+ version="2.0",
+ termsOfService="You are on your own.",
+ externalDocs=@ExternalDocs(description="Apache Juneau",url="http://juneau.apache.org")
+ )
+)
+@HtmlDocConfig(
+ navlinks={
+ "up: request:/..",
+ "options: servlet:/?method=OPTIONS",
+ "$W{UploadPhotoMenuItem}",
+ "source: $C{Source/gitHub}/org/apache/juneau/examples/rest/$R{servletClassSimple}.java"
+ },
+ aside={
+ "<div style='max-width:400px;min-width:200px' class='text'>",
+ " <p>Shows an example of using custom serializers and parsers to create REST interfaces over binary resources.</p>",
+ " <p>In this case, our resources are marshalled jpeg and png binary streams and are stored in an in-memory 'database' (also known as a <c>TreeMap</c>).</p>",
+ "</div>"
+ },
+ widgets={
+ UploadPhotoMenuItem.class
+ },
+ stylesheet="servlet:/htdocs/themes/dark.css"
+)
+@HtmlConfig(
+ // Make the anchor text on URLs be just the path relative to the servlet.
+ uriAnchorText="SERVLET_RELATIVE"
+)
+public class PhotosResource extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ // Our cache of photos
+ private Map<String,Photo> photos = new TreeMap<>();
+
+ @Override /* Servlet */
+ public void init() {
+ try (InputStream is = getClass().getResourceAsStream("photos/cat.jpg")) {
+ BufferedImage image = ImageIO.read(is);
+ Photo photo = new Photo("cat", image);
+ photos.put(photo.id, photo);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Our bean class for storing photos */
+ public static class Photo {
+ String id;
+ BufferedImage image;
+
+ Photo(String id, BufferedImage image) {
+ this.id = id;
+ this.image = image;
+ }
+
+ /**
+ * @return The URI of this photo.
+ * @throws URISyntaxException ID could not be converted to a URI.
+ */
+ public URI getURI() throws URISyntaxException {
+ return new URI("servlet:/" + id);
+ }
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // REST methods
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Show the list of all currently loaded photos
+ *
+ * @return The list of all currently loaded photos.
+ * @throws Exception Error occurred.
+ */
+ @RestMethod(
+ name=GET,
+ path="/",
+ summary="Show the list of all currently loaded photos"
+ )
+ public Collection<Photo> getAllPhotos() throws Exception {
+ return photos.values();
+ }
+
+ /**
+ * Shows how to use a custom serializer to serialize a BufferedImage object to a stream.
+ *
+ * @param id The photo ID.
+ * @return The image.
+ * @throws NotFound Image was not found.
+ */
+ @RestMethod(
+ name=GET,
+ path="/{id}",
+ serializers=ImageSerializer.class,
+ summary="Get a photo by ID",
+ description="Shows how to use a custom serializer to serialize a BufferedImage object to a stream."
+ )
+ @Response(
+ schema=@Schema(type="file")
+ )
+ public BufferedImage getPhoto(@Path("id") String id) throws NotFound {
+ Photo p = photos.get(id);
+ if (p == null)
+ throw new NotFound("Photo not found");
+ return p.image;
+ }
+
+ /**
+ * Shows how to use a custom parser to parse a stream into a BufferedImage object.
+ *
+ * @param id The photo ID.
+ * @param image Binary contents of image.
+ * @return <js>"OK"</jk> if successful.
+ * @throws Exception Error occurred.
+ */
+ @RestMethod(
+ name=PUT,
+ path="/{id}",
+ parsers=ImageParser.class,
+ summary="Add or overwrite a photo",
+ description="Shows how to use a custom parser to parse a stream into a BufferedImage object."
+ )
+ public String addPhoto(
+ @Path("id") String id,
+ @Body(
+ description="Binary contents of image.",
+ schema=@Schema(type="file")
+ )
+ BufferedImage image
+ ) throws Exception {
+ photos.put(id, new Photo(id, image));
+ return "OK";
+ }
+
+ /**
+ * Shows how to use a custom parser to parse a stream into a BufferedImage object.
+ *
+ * @param image Binary contents of image.
+ * @return The Photo bean.
+ * @throws Exception Error occurred.
+ */
+ @RestMethod(
+ name=POST,
+ path="/",
+ parsers=ImageParser.class,
+ summary="Add a photo",
+ description="Shows how to use a custom parser to parse a stream into a BufferedImage object."
+ )
+ public Photo setPhoto(
+ @Body(
+ description="Binary contents of image.",
+ schema=@Schema(type="file")
+ )
+ BufferedImage image
+ ) throws Exception {
+ Photo p = new Photo(UUID.randomUUID().toString(), image);
+ photos.put(p.id, p);
+ return p;
+ }
+
+ /**
+ * Upload a photo from a multipart form post.
+ *
+ * @param req HTTP request.
+ * @return Redirect to servlet root.
+ * @throws Exception Error occurred.
+ */
+ @RestMethod(
+ name=POST,
+ path="/upload",
+ matchers=MultipartFormDataMatcher.class,
+ summary="Upload a photo from a multipart form post",
+ description="Shows how to parse a multipart form post containing a binary field.",
+ swagger=@MethodSwagger(
+ parameters={
+ "{in:'formData', name:'id', description:'Unique identifier to assign to image.', type:'string', required:false},",
+ "{in:'formData', name:'file', description:'The binary contents of the image file.', type:'file', required:true}"
+ }
+ )
+ )
+ public SeeOtherRoot uploadFile(RestRequest req) throws Exception {
+ MultipartConfigElement multipartConfigElement = new MultipartConfigElement((String)null);
+ req.setAttribute("org.eclipse.jetty.multipartConfig", multipartConfigElement);
+ String id = UUID.randomUUID().toString();
+ BufferedImage img = null;
+ for (Part part : req.getParts()) {
+ switch (part.getName()) {
+ case "id":
+ id = IOUtils.read(part.getInputStream());
+ break;
+ case "file":
+ img = ImageIO.read(part.getInputStream());
+ }
+ }
+ addPhoto(id, img);
+ return new SeeOtherRoot(); // Redirect to the servlet root.
+ }
+
+ /**
+ * Removes a photo from the database.
+ *
+ * @param id ID of photo to remove.
+ * @return <js>"OK"</jk> if successful.
+ * @throws NotFound Photo was not found.
+ */
+ @RestMethod(
+ name=DELETE,
+ path="/{id}",
+ summary="Delete a photo by ID"
+ )
+ public String deletePhoto(@Path("id") String id) throws NotFound {
+ Photo p = photos.remove(id);
+ if (p == null)
+ throw new NotFound("Photo not found");
+ return "OK";
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Custom serializers and parsers.
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /** Serializer for converting images to byte streams */
+ public static class ImageSerializer extends OutputStreamSerializer {
+
+ /**
+ * Constructor.
+ * @param ps The property store containing all the settings for this object.
+ */
+ public ImageSerializer(PropertyStore ps) {
+ super(ps, null, "image/png,image/jpeg");
+ }
+
+ @Override /* Serializer */
+ public OutputStreamSerializerSession createSession(SerializerSessionArgs args) {
+ return new OutputStreamSerializerSession(args) {
+
+ @Override /* SerializerSession */
+ protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
+ RenderedImage image = (RenderedImage)o;
+ String mediaType = getProperty("mediaType", String.class, (String)null);
+ ImageIO.write(image, mediaType.substring(mediaType.indexOf('/')+1), out.getOutputStream());
+ }
+ };
+ }
+ }
+
+ /** Parser for converting byte streams to images */
+ public static class ImageParser extends InputStreamParser {
+
+ /**
+ * Constructor.
+ * @param ps The property store containing all the settings for this object.
+ */
+ public ImageParser(PropertyStore ps) {
+ super(ps, "image/png", "image/jpeg");
+ }
+
+ @Override /* Parser */
+ public InputStreamParserSession createSession(final ParserSessionArgs args) {
+ return new InputStreamParserSession(args) {
+
+ @Override /* ParserSession */
+ @SuppressWarnings("unchecked")
+ protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
+ return (T)ImageIO.read(pipe.getInputStream());
+ }
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/RootResources.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/RootResources.java
new file mode 100644
index 0000000..424b8f3
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/RootResources.java
@@ -0,0 +1,66 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore.rest;
+
+import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.widget.*;
+import org.apache.juneau.serializer.annotation.*;
+
+/**
+ * Sample REST resource showing how to implement a "router" resource page.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+@RestResource(
+ path="/*",
+ title="Root resources",
+ description="Example of a router resource page.",
+ children={
+ PetStoreResource.class
+ }
+)
+@HtmlDocConfig(
+ widgets={
+ ContentTypeMenuItem.class,
+ ThemeMenuItem.class
+ },
+ navlinks={
+ "options: ?method=OPTIONS",
+ "$W{ContentTypeMenuItem}",
+ "$W{ThemeMenuItem}",
+ "source: $C{Source/gitHub}/org/apache/juneau/examples/rest/$R{servletClassSimple}.java"
+ },
+ aside={
+ "<div style='max-width:400px' class='text'>",
+ " <p>This is an example of a 'router' page that serves as a jumping-off point to child resources.</p>",
+ " <p>Resources can be nested arbitrarily deep through router pages.</p>",
+ " <p>Note the <span class='link'>options</span> link provided that lets you see the generated swagger doc for this page.</p>",
+ " <p>Also note the <span class='link'>sources</span> link on these pages to view the source code for the page.</p>",
+ " <p>All content on pages in the UI are serialized POJOs. In this case, it's a serialized array of beans with 2 properties, 'name' and 'description'.</p>",
+ " <p>Other features (such as this aside) are added through annotations.</p>",
+ "</div>"
+ }
+)
+@SerializerConfig(
+ // For testing purposes, we want to use single quotes in all the serializers so it's easier to do simple
+ // String comparisons.
+ // You can apply any of the Serializer/Parser/BeanContext settings this way.
+ quoteChar="'"
+)
+public class RootResources extends BasicRestServletGroup {
+ private static final long serialVersionUID = 1L;
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/SqlQueryResource.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/SqlQueryResource.java
new file mode 100644
index 0000000..de015b0
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/SqlQueryResource.java
@@ -0,0 +1,249 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore.rest;
+
+import static org.apache.juneau.dto.html5.HtmlBuilder.*;
+import static org.apache.juneau.http.HttpMethodName.*;
+import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.rest.annotation.HookEvent.*;
+
+import java.sql.*;
+import java.util.*;
+
+import org.apache.juneau.jsonschema.annotation.ExternalDocs;
+import org.apache.juneau.config.*;
+import org.apache.juneau.dto.*;
+import org.apache.juneau.dto.html5.*;
+import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.annotation.Body;
+import org.apache.juneau.http.annotation.Query;
+import org.apache.juneau.http.annotation.Response;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.http.exception.*;
+import org.apache.juneau.rest.widget.*;
+
+/**
+ * Sample resource that shows how Juneau can serialize ResultSets.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+@RestResource(
+ path="/sql",
+ title="SQL query service",
+ description="Executes queries against the local derby '$C{SqlQueryResource/connectionUrl}' database",
+ swagger=@ResourceSwagger(
+ contact=@Contact(name="Juneau Developer",email="dev@juneau.apache.org"),
+ license=@License(name="Apache 2.0",url="http://www.apache.org/licenses/LICENSE-2.0.html"),
+ version="2.0",
+ termsOfService="You are on your own.",
+ externalDocs=@ExternalDocs(description="Apache Juneau",url="http://juneau.apache.org")
+ )
+)
+@HtmlDocConfig(
+ widgets={
+ ThemeMenuItem.class
+ },
+ navlinks={
+ "up: request:/..",
+ "options: servlet:/?method=OPTIONS",
+ "$W{ThemeMenuItem}",
+ "source: $C{Source/gitHub}/org/apache/juneau/examples/rest/$R{servletClassSimple}.java"
+ },
+ aside={
+ "<div style='min-width:200px' class='text'>",
+ " <p>An example of a REST interface over a relational database that serializes ResultSet objects.</p>",
+ " <p>Specify one or more queries delimited by semicolons.</p>",
+ " <h5>Examples:</h5>",
+ " <ul>",
+ " <li><a class='link' href='?sql=select+*+from+sys.systables'>Tables</a>",
+ " <li><a class='link' href='?sql=select+*+from+PetstorePet'>Pets</a>",
+ " <li><a class='link' href='?sql=select+*+from+PetstoreOrder'>Orders</a>",
+ " <li><a class='link' href='?sql=select+*+from+PetstoreUser'>Users</a>",
+ " </ul>",
+ "</div>"
+ },
+ stylesheet="servlet:/htdocs/themes/dark.css"
+)
+public class SqlQueryResource extends BasicRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ private String driver, connectionUrl;
+ private boolean allowUpdates, allowTempUpdates, includeRowNums;
+
+ /**
+ * Initializes the registry URL and rest client.
+ *
+ * @param builder The resource config.
+ */
+ @RestHook(INIT)
+ public void initConnection(RestContextBuilder builder) {
+ Config cf = builder.getConfig();
+
+ driver = cf.getString("SqlQueryResource/driver");
+ connectionUrl = cf.getString("SqlQueryResource/connectionUrl");
+ allowUpdates = cf.getBoolean("SqlQueryResource/allowUpdates", false);
+ allowTempUpdates = cf.getBoolean("SqlQueryResource/allowTempUpdates", false);
+ includeRowNums = cf.getBoolean("SqlQueryResource/includeRowNums", false);
+
+ try {
+ Class.forName(driver).newInstance();
+ } catch (Exception e) {
+ e.printStackTrace(System.err);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Displays the query entry page.
+ *
+ * @param sql Text to prepopulate the SQL query field with.
+ * @return The HTML div tag to serialize.
+ */
+ @RestMethod(
+ summary="Display the query entry page"
+ )
+ public Div get(
+ @Query(
+ name="sql",
+ description="Text to prepopulate the SQL query field with.",
+ example="select * from sys.systables"
+ )
+ String sql
+ ) {
+
+ return div(
+ script("text/javascript",
+ "// Quick and dirty function to allow tabs in textarea.",
+ "function checkTab(e) {",
+ " if (e.keyCode == 9) {",
+ " var t = e.target;",
+ " var ss = t.selectionStart, se = t.selectionEnd;",
+ " t.value = t.value.slice(0,ss).concat('\\t').concat(t.value.slice(ss,t.value.length));",
+ " e.preventDefault();",
+ " }",
+ "}",
+ "// Load results from IFrame into this document.",
+ "function loadResults(b) {",
+ " var doc = b.contentDocument || b.contentWindow.document;",
+ " var data = doc.getElementById('data') || doc.getElementsByTagName('body')[0];",
+ " document.getElementById('results').innerHTML = data.innerHTML;",
+ "}"
+ ),
+ form("servlet:/").method(POST).target("buf").children(
+ table(
+ tr(
+ th("Position (1-10000):").style("white-space:nowrap"),
+ td(input().name("pos").type("number").value(1)),
+ th("Limit (1-10000):").style("white-space:nowrap"),
+ td(input().name("limit").type("number").value(100)),
+ td(button("submit", "Submit"), button("reset", "Reset"))
+ ),
+ tr(
+ td().colspan(5).children(
+ textarea().name("sql").text(sql == null ? " " : sql).style("width:100%;height:200px;font-family:Courier;font-size:9pt;").onkeydown("checkTab(event)")
+ )
+ )
+ )
+ ),
+ br(),
+ div().id("results"),
+ iframe().name("buf").style("display:none").onload("parent.loadResults(this)")
+ );
+ }
+
+ /**
+ * Execute one or more queries.
+ *
+ * @param in
+ * Query input
+ * @return
+ * Query results.
+ * <br>Each entry in the array is a result of one query.
+ * <b>Each result can be a result set (for queries) or update count (for updates).
+ * @throws BadRequest Invalid SQL detected.
+ */
+ @RestMethod(
+ summary="Execute one or more queries"
+ )
+ @Response(
+ description="Query results.\nEach entry in the array is a result of one query.\nEach result can be a result set (for queries) or update count (for updates)."
+ )
+ public List<Object> post(
+ @Body(
+ description="Query input",
+ example="{sql:'select * from sys.systables',pos:1,limit:100}"
+ )
+ PostInput in
+ ) throws BadRequest {
+
+ List<Object> results = new LinkedList<>();
+
+ // Don't try to submit empty input.
+ if (isEmpty(in.sql))
+ return results;
+
+ if (in.pos < 1 || in.pos > 10000)
+ throw new BadRequest("Invalid value for position. Must be between 1-10000");
+ if (in.limit < 1 || in.limit > 10000)
+ throw new BadRequest("Invalid value for limit. Must be between 1-10000");
+
+ String sql = null;
+
+ // Create a connection and statement.
+ // If these fais, let the exception filter up as a 500 error.
+ try (Connection c = DriverManager.getConnection(connectionUrl)) {
+ c.setAutoCommit(false);
+ try (Statement st = c.createStatement()) {
+ for (String s : in.sql.split(";")) {
+ sql = s.trim();
+ if (! sql.isEmpty()) {
+ Object o = null;
+ if (allowUpdates || (allowTempUpdates && ! sql.matches("(?:i)commit.*"))) {
+ if (st.execute(sql)) {
+ try (ResultSet rs = st.getResultSet()) {
+ o = new ResultSetList(rs, in.pos, in.limit, includeRowNums);
+ }
+ } else {
+ o = st.getUpdateCount();
+ }
+ } else {
+ try (ResultSet rs = st.executeQuery(sql)) {
+ o = new ResultSetList(rs, in.pos, in.limit, includeRowNums);
+ }
+ }
+ results.add(o);
+ }
+ }
+ }
+ if (allowUpdates)
+ c.commit();
+ else if (allowTempUpdates)
+ c.rollback();
+ } catch (SQLException e) {
+ throw new BadRequest(e, "Invalid query: {0}", sql);
+ }
+
+ return results;
+ }
+
+ /** The parsed form post */
+ @SuppressWarnings("javadoc")
+ public static class PostInput {
+ public String sql = "";
+ public int pos = 1, limit = 100;
+ }
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/UploadPhotoMenuItem.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/UploadPhotoMenuItem.java
new file mode 100644
index 0000000..3847890
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/rest/UploadPhotoMenuItem.java
@@ -0,0 +1,61 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore.rest;
+
+import static org.apache.juneau.dto.html5.HtmlBuilder.*;
+import static org.apache.juneau.http.HttpMethodName.*;
+
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.widget.*;
+
+/**
+ * Menu item for uploading a Photo.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+public class UploadPhotoMenuItem extends MenuItemWidget {
+
+ @Override /* MenuItemWidget */
+ public String getLabel(RestRequest req, RestResponse res) throws Exception {
+ return "upload";
+ }
+
+ @Override /* Widget */
+ public Object getContent(RestRequest req, RestResponse res) throws Exception {
+ return div(
+ form().id("form").action("servlet:/upload").method(POST).enctype("multipart/form-data").children(
+ table(
+ tr(
+ th("ID:"),
+ td(input().name("id").type("text")),
+ td(new Tooltip("❓", "The unique identifier of the photo.", br(), "e.g. 'Fluffy'"))
+ ),
+ tr(
+ th("File:"),
+ td(input().name("file").type("file").accept("image/*")),
+ td(new Tooltip("❓", "The image file."))
+ ),
+ tr(
+ td().colspan(2).style("text-align:right").children(
+ button("reset", "Reset"),
+ button("button","Cancel").onclick("window.location.href='/'"),
+ button("submit", "Submit")
+ )
+ )
+ ).style("white-space:nowrap")
+ )
+ );
+ }
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/service/AbstractPersistenceService.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/service/AbstractPersistenceService.java
new file mode 100644
index 0000000..4e2aedf
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/service/AbstractPersistenceService.java
@@ -0,0 +1,273 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore.service;
+
+import java.util.*;
+
+import javax.persistence.*;
+
+import org.apache.juneau.pojotools.*;
+
+/**
+ * Superclass for DAOs that use the JPA entity manager.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+public class AbstractPersistenceService {
+
+ private final EntityManagerFactory entityManagerFactory;
+
+ /**
+ * Constructor.
+ */
+ public AbstractPersistenceService() {
+ entityManagerFactory = Persistence.createEntityManagerFactory("test");
+ }
+
+ /**
+ * Retrieves an entity manager session.
+ *
+ * @return The entity manager session.
+ */
+ protected EntityManager getEntityManager() {
+ return entityManagerFactory.createEntityManager();
+ }
+
+ /**
+ * Retrieves the specified JPA bean from the repository.
+ *
+ * @param em The entity manager to use to retrieve the bean.
+ * @param t The bean type to retrieve.
+ * @param id The primary key value.
+ * @return The JPA bean, or null if not found.
+ */
+ protected <T> T find(EntityManager em, Class<T> t, Object id) {
+ return em.find(t, id);
+ }
+
+ /**
+ * Same as {@link #find(EntityManager, Class, Object)} but uses a new entity manager.
+ *
+ * @param t The bean type to retrieve.
+ * @param id The primary key value.
+ * @return The JPA bean, or null if not found.
+ */
+ protected <T> T find(Class<T> t, Object id) {
+ return find(getEntityManager(), t, id);
+ }
+
+ /**
+ * Store the specified JPA bean in the repository.
+ *
+ * @param em The entity manager to use to store and merge the bean.
+ * @param t The bean to store.
+ * @return The merged JPA bean returned by the {@link EntityManager#merge(Object)} method, or null if the bean was null.
+ */
+ protected <T> T merge(EntityManager em, T t) {
+ if (t == null)
+ return null;
+ try {
+ EntityTransaction et = em.getTransaction();
+ et.begin();
+ t = em.merge(t);
+ et.commit();
+ return t;
+ } finally {
+ em.close();
+ }
+ }
+
+ /**
+ * Same as {@link #merge(EntityManager, Object)} but uses a new entity manager.
+ *
+ * @param t The bean to store.
+ * @return The merged JPA bean returned by the {@link EntityManager#merge(Object)} method, or null if the bean was null.
+ */
+ protected <T> T merge(T t) {
+ return merge(getEntityManager(), t);
+ }
+
+ /**
+ * Store the specified JPA beans in the repository.
+ *
+ * All values are persisted in the same transaction.
+ *
+ * @param em The entity manager to use to store and merge the beans.
+ * @param c The collection of beans to store.
+ * @return The merged JPA beans returned by the {@link EntityManager#merge(Object)} method.
+ */
+ protected <T> Collection<T> merge(EntityManager em, Collection<T> c) {
+ Collection<T> c2 = new ArrayList<>();
+ try {
+ EntityTransaction et = em.getTransaction();
+ et.begin();
+ for (T t : c)
+ c2.add(em.merge(t));
+ et.commit();
+ return c2;
+ } finally {
+ em.close();
+ }
+ }
+
+ /**
+ * Same as {@link #merge(EntityManager, Collection)} but uses a new entity manager.
+ *
+ * @param c The collection of beans to store.
+ * @return The merged JPA beans returned by the {@link EntityManager#merge(Object)} method.
+ */
+ protected <T> Collection<T> merge(Collection<T> c) {
+ return merge(getEntityManager(), c);
+ }
+
+ /**
+ * Remove the specified JPA bean from the repository.
+ *
+ * @param t The bean type to remove.
+ * @param id The primary key value.
+ */
+ protected <T> void remove(Class<T> t, Object id) {
+ EntityManager em = getEntityManager();
+ remove(em, find(em, t, id));
+ }
+
+ /**
+ * Remove the specified JPA bean from the repository.
+ *
+ * @param em The entity manager used to retrieve the bean.
+ * @param t The bean to remove. Can be null.
+ */
+ protected <T> void remove(EntityManager em, T t) {
+ if (t == null)
+ return;
+ try {
+ EntityTransaction et = em.getTransaction();
+ et.begin();
+ em.remove(t);
+ et.commit();
+ } finally {
+ em.close();
+ }
+ }
+
+ /**
+ * Runs a JPA query and returns the results.
+ *
+ * @param <T> The bean type.
+ * @param em The entity manager to use to retrieve the beans.
+ * @param query The JPA query.
+ * @param t The bean type.
+ * @param searchArgs Optional search arguments.
+ * @return The results.
+ */
+ protected <T> List<T> query(EntityManager em, String query, Class<T> t, SearchArgs searchArgs, PageArgs pageArgs) {
+ TypedQuery<T> q = em.createQuery(query, t);
+ if (pageArgs != null) {
+ q.setMaxResults(pageArgs.getLimit() == 0 ? 100 : pageArgs.getLimit());
+ q.setFirstResult(pageArgs.getPosition());
+ }
+ return em.createQuery(query, t).getResultList();
+ }
+
+ /**
+ * Same as {@link #query(EntityManager,String,Class,SearchArgs)} but uses a new entity manager.
+ *
+ * @param <T> The bean type.
+ * @param query The JPA query.
+ * @param t The bean type.
+ * @param searchArgs Optional search arguments.
+ * @return The results.
+ */
+ protected <T> List<T> query(String query, Class<T> t, SearchArgs searchArgs, PageArgs pageArgs) {
+ return query(getEntityManager(), query, t, searchArgs, pageArgs);
+ }
+
+ /**
+ * Runs a JPA parameterized query and returns the results.
+ *
+ * @param em The entity manager to use to retrieve the beans.
+ * @param query The JPA query.
+ * @param t The bean type.
+ * @param params The query parameter values.
+ * @return The results.
+ */
+ protected <T> List<T> query(EntityManager em, String query, Class<T> t, Map<String,Object> params) {
+ TypedQuery<T> tq = em.createQuery(query, t);
+ for (Map.Entry<String,Object> e : params.entrySet()) {
+ tq.setParameter(e.getKey(), e.getValue());
+ }
+ return tq.getResultList();
+ }
+
+ /**
+ * Same as {@link #query(EntityManager,String,Class,Map)} but uses a new entity manager.
+ *
+ * @param query The JPA query.
+ * @param t The bean type.
+ * @param params The query parameter values.
+ * @return The results.
+ */
+ protected <T> List<T> query(String query, Class<T> t, Map<String,Object> params) {
+ return query(getEntityManager(), query, t, params);
+ }
+
+ /**
+ * Runs a JPA update statement.
+ *
+ * @param em The entity manager to use to run the statement.
+ * @param query The JPA update statement.
+ * @return The number of rows modified.
+ */
+ protected int update(EntityManager em, String query) {
+ return em.createQuery(query).executeUpdate();
+ }
+
+ /**
+ * Same as {@link #update(EntityManager,String)} but uses a new entity manager.
+ *
+ * @param query The JPA update statement.
+ * @return The number of rows modified.
+ */
+ protected int update(String query) {
+ return update(getEntityManager(), query);
+ }
+
+ /**
+ * Runs a JPA parameterized update statement.
+ *
+ * @param em The entity manager to use to run the statement.
+ * @param query The JPA update statement.
+ * @param params The query parameter values.
+ * @return The number of rows modified.
+ */
+ protected int update(EntityManager em, String query, Map<String,Object> params) {
+ Query q = em.createQuery(query);
+ for (Map.Entry<String,Object> e : params.entrySet()) {
+ q.setParameter(e.getKey(), e.getValue());
+ }
+ return q.executeUpdate();
+ }
+
+ /**
+ * Same as {@link #update(EntityManager,String,Map)} but uses a new entity manager.
+ *
+ * @param query The JPA update statement.
+ * @param params The query parameter values.
+ * @return The number of rows modified.
+ */
+ protected int update(String query, Map<String,Object> params) {
+ return update(getEntityManager(), query, params);
+ }
+}
diff --git a/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/service/PetStoreService.java b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/service/PetStoreService.java
new file mode 100644
index 0000000..ac83c4c
--- /dev/null
+++ b/juneau-examples/juneau-examples-petstore/juneau-examples-petstore-server/src/main/java/org/apache/juneau/petstore/service/PetStoreService.java
@@ -0,0 +1,329 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.petstore.service;
+
+import static java.text.MessageFormat.*;
+
+import java.io.*;
+import java.util.*;
+
+import javax.persistence.*;
+
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.petstore.dto.*;
+import org.apache.juneau.pojotools.*;
+import org.apache.juneau.pojotools.SearchArgs;
+
+/**
+ * Pet store database application.
+ * <p>
+ * Uses JPA persistence to store and retrieve PetStore DTOs.
+ * JPA beans are defined in <c>META-INF/persistence.xml</c>.
+ *
+ * <ul class='seealso'>
+ * <li class='extlink'>{@source}
+ * </ul>
+ */
+public class PetStoreService extends AbstractPersistenceService {
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Initialization methods.
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Initialize the petstore database using JPA.
+ *
+ * @param w Console output.
+ * @return This object (for method chaining).
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException File could not be read from file system.
+ */
+ public PetStoreService initDirect(PrintWriter w) throws ParseException, IOException {
+
+ EntityManager em = getEntityManager();
+ EntityTransaction et = em.getTransaction();
+ JsonParser parser = JsonParser.create().build();
+
+ et.begin();
+
+ for (Pet x : em.createQuery("select X from PetstorePet X", Pet.class).getResultList()) {
+ em.remove(x);
+ w.println(format("Deleted pet: id={0}", x.getId()));
+ }
+ for (Order x : em.createQuery("select X from PetstoreOrder X", Order.class).getResultList()) {
+ em.remove(x);
+ w.println(format("Deleted order: id={0}", x.getId()));
+ }
+ for (User x : em.createQuery("select X from PetstoreUser X", User.class).getResultList()) {
+ em.remove(x);
+ w.println(format("Deleted user: username={0}", x.getUsername()));
+ }
+
+ et.commit();
+ et.begin();
+
+ for (Pet x : parser.parse(getStream("init/Pets.json"), Pet[].class)) {
+ x = em.merge(x);
+ w.println(format("Created pet: id={0}, name={1}", x.getId(), x.getName()));
+ }
+ for (Order x : parser.parse(getStream("init/Orders.json"), Order[].class)) {
+ x = em.merge(x);
+ w.println(format("Created order: id={0}", x.getId()));
+ }
+ for (User x: parser.parse(getStream("init/Users.json"), User[].class)) {
+ x = em.merge(x);
+ w.println(format("Created user: username={0}", x.getUsername()));
+ }
+
+ et.commit();
+
+ return this;
+ }
+
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Service methods.
+ //-----------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Returns the pet with the specified ID.
+ *
+ * @param id The pet ID.
+ * @return The pet with the specified ID. Never <jk>null</jk>.
+ * @throws IdNotFound If pet was not found.
+ */
+ public Pet getPet(long id) throws IdNotFound {
+ return find(Pet.class, id);
+ }
+
+ /**
+ * Returns the order with the specified ID.
+ *
+ * @param id The order ID.
+ * @return The order with the specified ID. Never <jk>null</jk>.
+ * @throws IdNotFound If order was not found.
+ */
+ public Order getOrder(long id) throws IdNotFound {
+ return find(Order.class, id);
+ }
+
+ /**
+ * Returns the user with the specified username.
+ *
+ * @param username The username.
+ * @return The user with the specified username. Never <jk>null</jk>.
+ * @throws InvalidUsername Username was not valid.
+ * @throws IdNotFound If order was not found.
+ */
+ public User getUser(String username) throws InvalidUsername, IdNotFound {
+ assertValidUsername(username);
+ return find(User.class, username);
+ }
+
+ /**
+ * Returns all pets in the database.
+ *
+ * @return All pets in the database.
+ */
+ public List<Pet> getPets() {
+ return query("select X from PetstorePet X", Pet.class, (SearchArgs)null, (PageArgs)null);
+ }
+
+ /**
+ * Returns all orders in the database.
+ *
+ * @return All orders in the database.
+ */
+ public List<Order> getOrders() {
+ return query("select X from PetstoreOrder X", Order.class, (SearchArgs)null, (PageArgs)null);
+ }
+
+ /**
+ * Returns all users in the database.
+ *
+ * @return All users in the database.
+ */
+ public List<User> getUsers() {
+ return query("select X from PetstoreUser X", User.class, (SearchArgs)null, (PageArgs)null);
+ }
+
+ /**
+ * Creates a new pet in the database.
+ *
+ * @param c The pet input data.
+ * @return a new {@link Pet} object.
+ */
+ public Pet create(CreatePet c) {
+ return merge(new Pet().status(PetStatus.AVAILABLE).apply(c));
+ }
+
+ /**
+ * Creates a new order in the database.
+ *
+ * @param c The order input data.
+ * @return a new {@link Order} object.
+ */
+ public Order create(CreateOrder c) {
+ return merge(new Order().status(OrderStatus.PLACED).apply(c));
+ }
+
+ /**
+ * Creates a new user in the database.
+ *
+ * @param c The user input data.
+ * @return a new {@link User} object.
+ */
+ public User create(User c) {
+ return merge(new User().apply(c));
+ }
+
+ /**
+ * Updates a pet in the database.
+ *
+ * @param u The update information.
+ * @return The updated {@link Pet} object.
+ * @throws IdNotFound Pet was not found.
+ */
+ public Pet update(UpdatePet u) throws IdNotFound {
+ EntityManager em = getEntityManager();
+ return merge(em, find(em, Pet.class, u.getId()).apply(u));
+ }
+
+ /**
+ * Updates an order in the database.
+ *
+ * @param o The update information.
+ * @return The updated {@link Order} object.
+ * @throws IdNotFound Order was not found.
+ */
+ public Order update(Order o) throws IdNotFound {
+ EntityManager em = getEntityManager();
+ return merge(em, find(em, Order.class, o.getId()).apply(o));
+ }
+
+ /**
+ * Updates a user in the database.
+ *
+ * @param u The update information.
+ * @return The updated {@link User} object.
+ * @throws IdNotFound User was not found.
+ * @throws InvalidUsername The username was not valid.
+ */
+ public User update(User u) throws IdNotFound, InvalidUsername {
+ assertValidUsername(u.getUsername());
+ EntityManager em = getEntityManager();
+ return merge(em, find(em, User.class, u.getUsername()).apply(u));
+ }
+
+ /**
+ * Removes a pet from the database.
+ *
+ * @param id The pet ID.
+ * @throws IdNotFound Pet was not found.
+ */
+ public void removePet(long id) throws IdNotFound {
+ EntityManager em = getEntityManager();
+ remove(em, find(em, Pet.class, id));
+ }
+
+ /**
+ * Removes an order from the database.
+ *
+ * @param id The order ID.
+ * @throws IdNotFound Order was not found.
+ */
+ public void removeOrder(long id) throws IdNotFound {
+ EntityManager em = getEntityManager();
+ remove(em, find(em, Order.class, id));
+ }
+
+ /**
+ * Removes a user from the database.
+ *
+ * @param username The username.
+ * @throws IdNotFound User was not found.
+ */
+ public void removeUser(String username) throws IdNotFound {
+ EntityManager em = getEntityManager();
+ remove(em, find(em, User.class, username));
+ }
+
+ /**
+ * Returns all pets with the specified statuses.
+ *
+ * @param status Pet statuses.
+ * @return Pets with the specified statuses.
+ */
+ public Collection<Pet> getPetsByStatus(PetStatus[] status) {
+ return getEntityManager()
+ .createQuery("select X from PetstorePet X where X.status in :status", Pet.class)
+ .setParameter("status", status)
+ .getResultList();
+ }
+
+ /**
+ * Returns all pets with the specified tags.
+ *
+ * @param tags Pet tags.
+ * @return Pets with the specified tags.
+ * @throws InvalidTag Tag name was invalid.
+ */
+ public Collection<Pet> getPetsByTags(String[] tags) throws InvalidTag {
+ return getEntityManager()
+ .createQuery("select X from PetstorePet X where X.tags in :tags", Pet.class)
+ .setParameter("tags", tags)
+ .getResultList();
+ }
+
+ /**
+ * Returns a summary of pet statuses and counts.
+ *
+ * @return A summary of pet statuses and counts.
+ */
+ public Map<PetStatus,Integer> getInventory() {
+ Map<PetStatus,Integer> m = new LinkedHashMap<>();
+ for (Pet p : getPets()) {
+ PetStatus ps = p.getStatus();
+ if (! m.containsKey(ps))
+ m.put(ps, 1);
+ else
+ m.put(ps, m.get(ps) + 1);
+ }
+ return m;
+ }
+
+ /**
+ * Returns <jk>true</jk> if the specified username and password is valid.
+ *
+ * @param username The username.
+ * @param password The password.
+ * @return <jk>true</jk> if the specified username and password is valid.
+ */
+ public boolean isValid(String username, String password) {
+ return getUser(username).getPassword().equals(password);
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Helper methods
+ //-----------------------------------------------------------------------------------------------------------------
+
+ private void assertValidUsername(String username) throws InvalidUsername {
+ if (username == null || ! username.matches("[\\w\\d]{3,8}"))
+ throw new InvalidUsername();
+ }
+
+ private InputStream getStream(String fileName) {
+ return getClass().getResourceAsStream(fileName);
+ }
+}
\ No newline at end of file