Commit c3fd35a1 by Patryk Czarnik

rest_serwer - jako jeden moduł z całym kodem

parent 9f871f93
/.project
/.settings/
target/
.project
.classpath
.settings/
*.iml
.idea/
......@@ -6,4 +6,35 @@
<artifactId>PC39-Wielomodulowy</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<!-- To jest "parent project" wobec konkretnych projektów, które dodamy tu jako moduły.
W tym pliku podaje się ogólne ustawienia. Można m.in. użyć pluginManagement i dependencyManagement
aby ustalić wersje wtyczek i zależności.
-->
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<modules>
<module>rest_serwer</module>
</modules>
</project>
\ No newline at end of file
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>pl.alx.kjava</groupId>
<artifactId>PC39-Wielomodulowy</artifactId>
<version>1.0</version>
</parent>
<artifactId>rest_serwer</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>fop</artifactId>
<version>2.8</version>
<exclusions>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
</build>
</project>
package sklep.db;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import sklep.model.Customer;
public class CustomerDAO {
private final DBConnection db;
CustomerDAO(DBConnection db) {
this.db = db;
}
public Customer findByEmail(String email) throws DBException, RecordNotFound {
final String sql = "SELECT * FROM customers WHERE customer_email = ?";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return customerFromRS(rs);
} else {
throw new RecordNotFound("Cannot find customer with email " + email);
}
}
} catch (SQLException e) {
throw new DBException("SQL error in CustomerDAO.findById: " + e.getMessage(), e);
}
}
public List<Customer> readAll() throws DBException {
final String sql = "SELECT * FROM customers";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
try (ResultSet rs = stmt.executeQuery()) {
return customerListFromRS(rs);
}
} catch (SQLException e) {
throw new DBException("SQL error in CustomerDAO.readAll: " + e.getMessage(), e);
}
}
private List<Customer> customerListFromRS(ResultSet rs) throws SQLException {
List<Customer> records = new ArrayList<>();
while (rs.next()) {
Customer product = customerFromRS(rs);
records.add(product);
}
return records;
}
private Customer customerFromRS(ResultSet rs) throws SQLException {
return new Customer(
rs.getString("customer_email"),
rs.getString("customer_name"),
rs.getString("phone_number"),
rs.getString("address"),
rs.getString("postal_code"),
rs.getString("city"));
}
public void insert(Customer customer) throws DBException {
// używać gdy obiekt ma wpisane ID (tu: email)
final String sql = "INSERT INTO customers("
+ "customer_email, customer_name, phone_number, address, postal_code, city)"
+ " VALUES (?, ?, ?, ?, ?, ?)";
try(PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setString(1, customer.getEmail());
stmt.setString(2, customer.getName());
stmt.setString(3, customer.getPhoneNumber());
stmt.setString(4, customer.getAddress());
stmt.setString(5, customer.getPostalCode());
stmt.setString(6, customer.getCity());
stmt.executeUpdate();
} catch (SQLException e) {
throw new DBException("Error during INSERT CUSTOMER", e);
}
}
public boolean update(Customer customer) throws DBException {
final String sql = "UPDATE customers SET "
+ " customer_name=?, phone_number=?, address=?, postal_code=?, city=?"
+ " WHERE customer_email = ?";
try(PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setString(1, customer.getName());
stmt.setString(2, customer.getPhoneNumber());
stmt.setString(3, customer.getAddress());
stmt.setString(4, customer.getPostalCode());
stmt.setString(5, customer.getCity());
stmt.setString(6, customer.getEmail());
int count = stmt.executeUpdate();
return count > 0;
} catch (SQLException e) {
throw new DBException("Error during UPDATE CUSTOMER", e);
}
}
public void save(Customer customer) throws DBException {
if(customer.getEmail() == null) {
throw new IllegalArgumentException("Customer email cannot be null");
} else {
if(! update(customer)) {
insert(customer);
}
}
}
public boolean delete(String email) throws DBException {
final String sql = "DELETE FROM customers WHERE customer_email = ?";
try(PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setString(1, email);
int count = stmt.executeUpdate();
return count > 0;
} catch (SQLException e) {
throw new DBException("Error during DELETE CUSTOMER", e);
}
}
public boolean delete(Customer customer) throws DBException {
return delete(customer.getEmail());
}
}
package sklep.db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class DBConnection implements AutoCloseable {
private Connection sqlConnection;
private DBConnection(Connection sqlConnection) {
this.sqlConnection = sqlConnection;
}
public static DBConnection open() throws DBException {
return open(false);
}
public static DBConnection open(boolean autoCommit) throws DBException {
try {
Properties props = DBSettings.load();
if(props.containsKey("driver_class")) {
Class.forName(props.getProperty("driver_class"));
}
Connection c = DriverManager.getConnection(props.getProperty("url") , props);
c.setAutoCommit(autoCommit);
return new DBConnection(c);
} catch (ClassNotFoundException | SQLException e) {
throw new DBException("Cannot connect to postgresql: " + e, e);
}
}
public static DBConnection openLocalhost() throws DBException {
try {
Connection c = DriverManager.getConnection("jdbc:postgresql://localhost/sklep", "kurs", "abc123");
c.setAutoCommit(false);
return new DBConnection(c);
} catch (SQLException e) {
throw new DBException("Cannot connect to postgresql: " + e, e);
}
}
@Override
public void close() {
try {
if (sqlConnection != null) {
sqlConnection.close();
sqlConnection = null;
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getSqlConnection() {
return sqlConnection;
}
public void commit() throws DBException {
try {
sqlConnection.commit();
} catch (SQLException e) {
throw new DBException("Error during commit: " + e.getMessage(), e);
}
}
public void rollback() throws DBException {
try {
sqlConnection.rollback();
} catch (SQLException e) {
throw new DBException("Error during rollback: " + e.getMessage(), e);
}
}
public ProductDAO productDAO() {
return new ProductDAO(this);
}
public CustomerDAO customerDAO() {
return new CustomerDAO(this);
}
public OrderDAO orderDAO() {
return new OrderDAO(this);
}
}
package sklep.db;
public class DBException extends SklepException {
public DBException() {
super();
}
public DBException(String message, Throwable cause) {
super(message, cause);
}
public DBException(String message) {
super(message);
}
}
package sklep.db;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class DBSettings {
public static final String DB_SETTINGS_SYSTEM_PROPERTY = "sklep.db_settings_location";
private static final String INTERNAL_DEFAULT_PROPERTIES = "/sklep.properties";
private static DBSettings dbSettings; // singleton
private final Properties props;
private DBSettings() throws DBException {
props = new Properties();
String systemProperty = System.getProperty(DB_SETTINGS_SYSTEM_PROPERTY);
try(InputStream input = systemProperty != null
? new FileInputStream(new File(systemProperty))
: DBSettings.class.getResourceAsStream(INTERNAL_DEFAULT_PROPERTIES) ) {
props.load(input);
} catch (IOException e) {
//e.printStackTrace();
throw new DBException("Cannot read settings. " + e, e);
}
}
public static synchronized DBSettings getInstance() throws DBException {
// Dla klasy typu "singleton" w aplikacji powstaje tylko jedna instancja (obiekt) tej klasy.
// Dostęp do tego obiektu odbywa się poprzez metodę statyczną taką jak ta.
// Tutaj mamy "leniwą inicjalizację", czyli obiekt jest tworzony przy pierwszej próbie dostepu.
if(dbSettings == null) {
dbSettings = new DBSettings();
}
return dbSettings;
}
public Properties getProperties() {
return props;
}
public static Properties load() throws DBException {
return getInstance().getProperties();
}
}
package sklep.db;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import sklep.model.Order;
import sklep.model.OrderProduct;
public class OrderDAO {
private static final String[] ID_COLUMNS = {"order_id", "order_date"};
private final DBConnection db;
OrderDAO(DBConnection db) {
this.db = db;
}
public Order findById(int orderId) throws DBException, RecordNotFound {
final String sql = "SELECT * FROM orders WHERE order_id = ?";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setInt(1, orderId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return orderFromRS(rs);
} else {
throw new RecordNotFound("Cannot find order with id " + orderId);
}
}
} catch (SQLException e) {
throw new DBException("SQL error in OrderDAO.findById: " + e.getMessage(), e);
}
}
public List<Order> readAll() throws DBException {
final String sql = "SELECT * FROM orders ORDER BY order_id";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
try (ResultSet rs = stmt.executeQuery()) {
return orderListFromRS(rs);
}
} catch (SQLException e) {
throw new DBException("SQL error in OrderDAO.readAll: " + e.getMessage(), e);
}
}
public List<Order> customerOrders(String email) throws DBException {
final String sql = "SELECT * FROM orders WHERE customer_email = ? ORDER BY order_id";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
return orderListFromRS(rs);
}
} catch (SQLException e) {
throw new DBException("SQL error in OrderDAO.customerOrders: " + e.getMessage(), e);
}
}
private List<Order> orderListFromRS(ResultSet rs) throws SQLException, DBException {
List<Order> orders = new ArrayList<>();
while (rs.next()) {
Order order = orderFromRS(rs);
orders.add(order);
}
return orders;
}
private Order orderFromRS(ResultSet rs) throws SQLException, DBException {
Order order = Order.ofDbFields(
rs.getInt("order_id"),
rs.getString("customer_email"),
rs.getTimestamp("order_date"),
rs.getString("status"));
order.addProducts(orderProductsForOrder(order.getOrderId()));
return order;
}
List<OrderProduct> orderProductsForOrder(int orderId) throws DBException {
final String sql = "SELECT * FROM order_products WHERE order_id = ? ORDER BY product_id";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setInt(1, orderId);
try (ResultSet rs = stmt.executeQuery()) {
List<OrderProduct> ops = new ArrayList<>();
while(rs.next()) {
ops.add(orderProductFromRS(rs));
}
return ops;
}
} catch (SQLException e) {
throw new DBException("SQL error in OrderDAO.customerOrders: " + e.getMessage(), e);
}
}
private OrderProduct orderProductFromRS(ResultSet rs) throws SQLException {
return new OrderProduct(rs.getInt("order_id"), rs.getInt("product_id"), rs.getInt("quantity"), rs.getBigDecimal("actual_price"));
}
}
package sklep.db;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import sklep.model.Product;
/* DAO - Data Access Object(s)
Dostęp do baz danych oparty o takie zasady:
tabelom bazodanowym odpowiadają klasy w naszej aplikacji
np. dla tabeli products mamy klasę Product (w pakiecie model)
dla takie pary tabela products + klasa Product tworzymy klasę narzędziową ProductDAO , której zadaniem jest obsługa tej tabeli: odczyt, zapis, wyszukiwanie, i inne operacje jeśli są potrzebne.
*/
public class ProductDAO {
private static final BigDecimal MAX_PRICE = new BigDecimal(1_000_000_000);
private static final String[] ID_COLUMNS = {"product_id"};
private final DBConnection db;
ProductDAO(DBConnection db) {
this.db = db;
}
public Product findById(int productId) throws DBException, RecordNotFound {
final String sql = "SELECT * FROM products WHERE product_id = ?";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setInt(1, productId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return productFromRS(rs);
} else {
throw new RecordNotFound("Cannot find product with id " + productId);
}
}
} catch (SQLException e) {
throw new DBException("SQL error in ProductDAO.findById: " + e.getMessage(), e);
}
}
public List<Product> readAll() throws DBException {
final String sql = "SELECT * FROM products ORDER BY product_id";
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
try (ResultSet rs = stmt.executeQuery()) {
return productListFromRS(rs);
}
} catch (SQLException e) {
throw new DBException("SQL error in ProductDAO.readAll: " + e.getMessage(), e);
}
}
public List<Product> findByPrice(BigDecimal minPrice, BigDecimal maxPrice) throws DBException {
final String sql = "SELECT * FROM products WHERE price BETWEEN ? AND ? ORDER BY product_id";
if(minPrice == null)
minPrice = BigDecimal.ZERO;
if(maxPrice == null)
maxPrice = MAX_PRICE;
try (PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setBigDecimal(1, minPrice);
stmt.setBigDecimal(2, maxPrice);
try (ResultSet rs = stmt.executeQuery()) {
return productListFromRS(rs);
}
} catch (SQLException e) {
throw new DBException("SQL error in ProductDAO.findByPrice: " + e.getMessage(), e);
}
}
private List<Product> productListFromRS(ResultSet rs) throws SQLException {
List<Product> products = new ArrayList<>();
while (rs.next()) {
Product product = productFromRS(rs);
products.add(product);
}
return products;
}
private Product productFromRS(ResultSet rs) throws SQLException {
return new Product(
rs.getInt("product_id"),
rs.getString("product_name"),
rs.getBigDecimal("price"),
rs.getBigDecimal("vat"),
rs.getString("description"));
}
public void insert(Product product) throws DBException {
// używać gdy obiekt ma wpisane ID
final String sql = "INSERT INTO products("
+ " product_id, product_name, price, vat, description)"
+ " VALUES (?, ?, ?, ?, ?)";
try(PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setInt(1, product.getProductId());
stmt.setString(2, product.getProductName());
stmt.setBigDecimal(3, product.getPrice());
stmt.setBigDecimal(4, product.getVat());
stmt.setString(5, product.getDescription());
stmt.executeUpdate();
} catch (SQLException e) {
throw new DBException("Error during INSERT PRODUCT", e);
}
}
public void insertNew(Product product) throws DBException {
// używać gdy obiekt nie ma wpisanego ID (productID == null)
final String sql = "INSERT INTO products("
+ " product_name, price, vat, description)"
+ " VALUES (?, ?, ?, ?)";
try(PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql, ID_COLUMNS)) {
stmt.setString(1, product.getProductName());
stmt.setBigDecimal(2, product.getPrice());
stmt.setBigDecimal(3, product.getVat());
stmt.setString(4, product.getDescription());
stmt.executeUpdate();
try (ResultSet rs = stmt.getGeneratedKeys()) {
if(rs.next()) {
// w obiekcie, który mamy w pamięci, uzupełniamy brakujące ID na podstawie tego, co wygenerowała baza
product.setProductId(rs.getInt(1));
}
};
} catch (SQLException e) {
throw new DBException("Error during INSERT PRODUCT", e);
}
}
public boolean update(Product product) throws DBException {
final String sql = "UPDATE products SET "
+ " product_name = ?, price = ?, vat = ?, description = ?"
+ " WHERE product_id = ?";
try(PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setString(1, product.getProductName());
stmt.setBigDecimal(2, product.getPrice());
stmt.setBigDecimal(3, product.getVat());
stmt.setString(4, product.getDescription());
stmt.setInt(5, product.getProductId());
int count = stmt.executeUpdate();
return count > 0;
} catch (SQLException e) {
throw new DBException("Error during UPDATE PRODUCT", e);
}
}
public void save(Product product) throws DBException {
if(product.getProductId() == null) {
// wstawiamy nowy rekord korzystajac z sekwecji
insertNew(product);
} else {
if(! update(product)) {
insert(product);
}
}
}
public boolean delete(int productId) throws DBException {
final String sql = "DELETE FROM products WHERE product_id = ?";
try(PreparedStatement stmt = db.getSqlConnection().prepareStatement(sql)) {
stmt.setInt(1, productId);
int count = stmt.executeUpdate();
return count > 0;
} catch (SQLException e) {
throw new DBException("Error during DELETE PRODUCT", e);
}
}
public boolean delete(Product product) throws DBException {
return delete(product.getProductId());
}
}
package sklep.db;
public class RecordNotFound extends SklepException {
public RecordNotFound() {
super();
}
public RecordNotFound(String message) {
super(message);
}
}
package sklep.db;
public class SklepException extends Exception {
public SklepException() {
super();
}
public SklepException(String message, Throwable cause) {
super(message, cause);
}
public SklepException(String message) {
super(message);
}
public SklepException(Throwable cause) {
super(cause);
}
}
package sklep.db;
import java.util.List;
import sklep.model.Product;
public class ZwyklyOdczyt_DAO {
public static void main(String[] args) {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
List<Product> products = productDAO.readAll();
for (Product product : products) {
System.out.println(product);
}
} catch (DBException e) {
e.printStackTrace();
}
}
}
package sklep.db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class ZwyklyOdczyt_JDBC {
public static void main(String[] args) {
String url = "jdbc:postgresql://localhost:5432/sklep";
String sql = "SELECT * FROM products";
try(Connection c = DriverManager.getConnection(url, "kurs", "abc123");
PreparedStatement stmt= c.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while(rs.next()) {
System.out.printf("%d: %s za cenę %s\n",
rs.getInt("product_id"), rs.getString("product_name"), rs.getBigDecimal("price"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
package sklep.model;
import java.util.Objects;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Customer {
@XmlAttribute
private String email;
private String name;
@XmlElement(name="phone")
private String phoneNumber;
private String address;
@XmlElement(name="postal-code")
private String postalCode;
private String city;
public Customer() {
}
public Customer(String email, String name, String phone, String address, String postalCode, String city) {
this.email = email;
this.name = name;
this.phoneNumber = phone;
this.address = address;
this.postalCode = postalCode;
this.city = city;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phone) {
this.phoneNumber = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
public String toString() {
return "Customer [email=" + email + ", name=" + name + ", phone=" + phoneNumber + ", address=" + address
+ ", postalCode=" + postalCode + ", city=" + city + "]";
}
@Override
public int hashCode() {
return Objects.hash(email, name, address, city, phoneNumber, postalCode);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Customer other = (Customer) obj;
return Objects.equals(email, other.email) && Objects.equals(name, other.name)
&& Objects.equals(address, other.address) && Objects.equals(city, other.city)
&& Objects.equals(phoneNumber, other.phoneNumber) && Objects.equals(postalCode, other.postalCode);
}
}
package sklep.model;
import java.time.LocalDateTime;
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
public class LocalDateTimeAdapter extends XmlAdapter<String, LocalDateTime> {
@Override
public LocalDateTime unmarshal(String s) throws Exception {
return LocalDateTime.parse(s);
}
@Override
public String marshal(LocalDateTime dt) throws Exception {
return dt.toString();
}
}
package sklep.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlElementWrapper;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlRootElement
public class Order {
@XmlAttribute(name="id")
private Integer orderId;
@XmlAttribute(name="customer-email")
private String customerEmail;
@XmlElement(name="order-date")
@XmlJavaTypeAdapter(LocalDateTimeAdapter.class)
private LocalDateTime orderDate;
@XmlAttribute(name="status")
private Status orderStatus;
@XmlElementWrapper(name="products")
@XmlElement(name="product")
public final List<OrderProduct> products = new ArrayList<>();
public Order() {
}
public Order(Integer orderId, String customerEmail, LocalDateTime orderDate, Status orderStatus) {
this.orderId = orderId;
this.customerEmail = customerEmail;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
}
public static Order ofDbFields(int orderId, String customerEmail, java.sql.Timestamp orderDate, String orderStatus) {
return new Order(orderId, customerEmail,
orderDate.toLocalDateTime(),
Status.valueOf(orderStatus.toUpperCase()));
}
public Integer getOrderId() {
return orderId;
}
public void setOrderId(Integer orderId) {
this.orderId = orderId;
}
public String getCustomerEmail() {
return customerEmail;
}
public void setCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
}
public LocalDateTime getOrderDate() {
return orderDate;
}
public void setOrderDate(LocalDateTime orderDate) {
this.orderDate = orderDate;
}
public Status getOrderStatus() {
return orderStatus;
}
public void setOrderStatus(Status orderStatus) {
this.orderStatus = orderStatus;
}
public List<OrderProduct> getProducts() {
return Collections.unmodifiableList(products);
}
public void addProduct(OrderProduct product) {
this.products.add(product);
}
public void addProducts(Collection<OrderProduct> products) {
this.products.addAll(products);
}
public void setProducts(Collection<OrderProduct> products) {
this.products.clear();
this.products.addAll(products);
}
@Override
public String toString() {
return "Order [orderId=" + orderId + ", customerEmail=" + customerEmail + ", orderDate=" + orderDate
+ ", orderStatus=" + orderStatus + "]";
}
@Override
public int hashCode() {
return Objects.hash(customerEmail, orderDate, orderId, orderStatus);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Order other = (Order) obj;
return Objects.equals(customerEmail, other.customerEmail) && Objects.equals(orderDate, other.orderDate)
&& Objects.equals(orderId, other.orderId) && orderStatus == other.orderStatus;
}
public enum Status {
NEW,
CONFIRMED,
PAID,
SHIPPED,
CLOSED,
RETURNED,
;
}
}
package sklep.model;
import java.math.BigDecimal;
import java.util.Objects;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlTransient;
public class OrderProduct {
@XmlTransient
private Integer orderId;
@XmlAttribute(name="id")
private Integer productId;
private int quantity;
@XmlElement(name="actual-price")
private BigDecimal actualPrice;
public OrderProduct() {
}
public OrderProduct(Integer orderId, Integer productId, int quantity, BigDecimal actualPrice) {
this.orderId = orderId;
this.productId = productId;
this.quantity = quantity;
this.actualPrice = actualPrice;
}
public static OrderProduct of(Integer orderId, Product product, int quantity) {
return new OrderProduct(orderId, product.getProductId(), quantity, product.getPrice());
}
public Integer getOrderId() {
return orderId;
}
public void setOrderId(Integer orderId) {
this.orderId = orderId;
}
public Integer getProductId() {
return productId;
}
public void setProductId(Integer productId) {
this.productId = productId;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public BigDecimal getActualPrice() {
return actualPrice;
}
public void setActualPrice(BigDecimal actualPrice) {
this.actualPrice = actualPrice;
}
@Override
public String toString() {
return "OrderProduct [orderId=" + orderId + ", productId=" + productId + ", quantity=" + quantity
+ ", actualPrice=" + actualPrice+ "]";
}
@Override
public int hashCode() {
return Objects.hash(orderId, productId, quantity, actualPrice);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
OrderProduct other = (OrderProduct) obj;
return Objects.equals(orderId, other.orderId) && Objects.equals(productId, other.productId)
&& quantity == other.quantity
&& Objects.equals(actualPrice, other.actualPrice);
}
}
package sklep.model;
import java.math.BigDecimal;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlValue;
@XmlRootElement
public class Price {
@XmlValue
private BigDecimal value;
public Price() {
this.value = BigDecimal.ZERO;
}
public Price(BigDecimal value) {
this.value = value;
}
public BigDecimal getValue() {
return value;
}
public void setValue(BigDecimal value) {
this.value = value;
}
@Override
public String toString() {
return value.toString();
}
}
package sklep.model;
import java.math.BigDecimal;
import java.util.Objects;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Product {
@XmlAttribute(name="id")
private Integer productId;
@XmlElement(name="product-name")
private String productName;
private BigDecimal price;
private BigDecimal vat;
private String description;
public Product() {
}
public Product(Integer productId, String productName, BigDecimal price, BigDecimal vat, String description) {
this.productId = productId;
this.productName = productName;
this.price = price;
this.vat = vat;
this.description = description;
}
public Integer getProductId() {
return productId;
}
public void setProductId(Integer productId) {
this.productId = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public BigDecimal getVat() {
return vat;
}
public void setVat(BigDecimal vat) {
this.vat = vat;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public int hashCode() {
return Objects.hash(description, price, vat, productId, productName);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Product other = (Product) obj;
return Objects.equals(productId, other.productId) && Objects.equals(productName, other.productName)
&& Objects.equals(price, other.price)
&& Objects.equals(vat, other.vat)
&& Objects.equals(description, other.description);
}
@Override
public String toString() {
return "Product [productId=" + productId + ", productName=" + productName + ", price=" + price + ", vat=" + vat
+ ", description=" + description + "]";
}
public String toHtml() {
return String.format("<div class='product'>"
+ "<h2>%s</h2>"
+ "<p>(nr %d)</p>"
+ "<p>Cena: <strong>%,.2f PLN</strong></p>"
+ "<p>%s</p>"
+ "</div>",
getProductName(),
getProductId(),
getPrice(),
getDescription());
}
}
package sklep.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="products")
public class ProductList {
@XmlElement(name="product")
private final List<Product> products = new ArrayList<>();
public ProductList() {
// zostawia pustą listę
}
public ProductList(Collection<Product> products) {
this.products.addAll(products);
}
public List<Product> getProducts() {
return Collections.unmodifiableList(this.products);
}
public void setProducts(Collection<Product> products) {
this.products.clear();
this.products.addAll(products);
}
@Override
public String toString() {
return this.products.toString();
}
}
@XmlAccessorType(XmlAccessType.FIELD)
package sklep.model;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
package sklep.photo;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import sklep.db.DBException;
import sklep.db.DBSettings;
import sklep.db.RecordNotFound;
public class PhotoUtil {
private static final String EXT = ".jpg";
public static File getFile(int productId) throws DBException, RecordNotFound {
Path path = getPath(productId);
File file = path.toFile();
if(file.exists()) {
return file;
} else {
throw new RecordNotFound("Cannot read photo for product id = " + productId);
}
}
public static byte[] readBytes(int productId) throws DBException, RecordNotFound {
Path path = getPath(productId);
try {
return Files.readAllBytes(path);
} catch (IOException e) {
// System.err.println(e);
throw new RecordNotFound("Cannot read photo for product id = " + productId);
}
}
public static void writeStream(int productId, InputStream inputStream) {
try {
Path path = getPath(productId);
Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
// wypisujemy błąd, ale metoda kończy się normalnie
e.printStackTrace();
}
}
private static Path getPath(int productId) throws DBException {
String dir = DBSettings.load().getProperty("photo_dir");
String fileName = productId + EXT;
return Paths.get(dir, fileName);
}
}
package sklep.rest;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("")
public class AplikacjaRestowa extends Application {
}
// W tej wersji cały projekt jest "jedną wielką usługą REST".
// Ale czasami może być być tak, że jest to aplikacja webowa, a tylko jej część działa na asadzie REST.
// stronka.html
// a JS z tej stronki wywołuje zaoytania RESTOWe, aby w formacie JSON pobierać sobie dane
// obsługa zapytań RESTowych może być umieszczona w podkatalogu, np. /api
// → wtedy miałoby sens napisać @ApplicationPath("/api")
package sklep.rest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
// Technologia JAX-RS, część Java EE (Jakarata EE)
// Obsługa zapytań HTTP w taki sposób, aby wygodnie realizowało się usługi typu REST.
@Path("/hello")
public class Hello {
@GET
public String hello() {
return "Hello REST";
}
}
package sklep.rest;
import java.util.List;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import sklep.model.Product;
// Tak wygląda klasa wygenerowana przez polecenie "New JAX-RS Resource" w Eclipse z dodatkiem wtyczki JBossTolls (JAX-RS Tools)
// Komentuję adnotacje przed klasą, aby nie wpływało to na działanie aplikacji.
// @RequestScoped
// @Path("/products")
// @Produces({ "application/xml", "application/json" })
// @Consumes({ "application/xml", "application/json" })
public class ProductEndpoint {
@POST
public Response create(final Product product) {
//TODO: process the given product
//you may want to use the following return statement, assuming that Product#getId() or a similar method
//would provide the identifier to retrieve the created Product resource:
//return Response.created(UriBuilder.fromResource(ProductEndpoint.class).path(String.valueOf(product.getId())).build()).build();
return Response.created(null).build();
}
@GET
@Path("/{id:[0-9][0-9]*}")
public Response findById(@PathParam("id") final Long id) {
//TODO: retrieve the product
Product product = null;
if (product == null) {
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok(product).build();
}
@GET
public List<Product> listAll(@QueryParam("start") final Integer startPosition,
@QueryParam("max") final Integer maxResult) {
//TODO: retrieve the products
final List<Product> products = null;
return products;
}
@PUT
@Path("/{id:[0-9][0-9]*}")
public Response update(@PathParam("id") Long id, final Product product) {
//TODO: process the given product
return Response.noContent().build();
}
@DELETE
@Path("/{id:[0-9][0-9]*}")
public Response deleteById(@PathParam("id") final Long id) {
//TODO: process the product matching by the given id
return Response.noContent().build();
}
}
package sklep.rest;
import java.util.List;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import sklep.db.CustomerDAO;
import sklep.db.DBConnection;
import sklep.db.DBException;
import sklep.db.RecordNotFound;
import sklep.model.Customer;
@RequestScoped
@Path("/customers")
@Produces({ "application/xml", "application/json" })
@Consumes({ "application/xml", "application/json" })
public class RCustomer {
@POST
public Response save(final Customer customer) throws DBException {
try(DBConnection db = DBConnection.open()) {
CustomerDAO customerDAO = db.customerDAO();
customerDAO.save(customer);
db.commit();
return Response.created(UriBuilder.fromResource(RCustomer.class).path(String.valueOf(customer.getEmail())).build()).build();
}
}
@GET
@Path("/{id}")
public Customer findById(@PathParam("id") final String email) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
CustomerDAO customerDAO = db.customerDAO();
return customerDAO.findByEmail(email);
}
}
@GET
public List<Customer> listAll() throws DBException {
try(DBConnection db = DBConnection.open()) {
CustomerDAO customerDAO = db.customerDAO();
return customerDAO.readAll();
}
}
@PUT
@Path("/{id}")
public Response update(@PathParam("id") String email, final Customer customer) throws DBException {
try(DBConnection db = DBConnection.open()) {
CustomerDAO customerDAO = db.customerDAO();
customer.setEmail(email);
customerDAO.save(customer);
db.commit();
}
return Response.noContent().build();
}
@DELETE
@Path("/{id}")
public Response deleteById(@PathParam("id") String email) throws DBException {
try(DBConnection db = DBConnection.open()) {
CustomerDAO customerDAO = db.customerDAO();
customerDAO.delete(email);
db.commit();
}
return Response.noContent().build();
}
}
package sklep.rest;
import java.net.URI;
import java.util.List;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriBuilder;
import sklep.db.CustomerDAO;
import sklep.db.DBConnection;
import sklep.db.DBException;
import sklep.db.OrderDAO;
import sklep.db.RecordNotFound;
import sklep.model.Customer;
import sklep.model.Order;
@RequestScoped
@Path("/orders")
@Produces({ "application/xml", "application/json" })
@Consumes({ "application/xml", "application/json" })
public class ROrder {
@GET
public List<Order> listAll() throws DBException {
try(DBConnection db = DBConnection.open()) {
OrderDAO orderDAO = db.orderDAO();
return orderDAO.readAll();
}
}
@GET
@Path("/{id:[0-9][0-9]*}")
public Response findById(@PathParam("id") final Integer id) {
// Klasa Response pozwala nam z pełną precyzją przygotować odpowiedź, która ma zostać odesłana klientowi.
// W przypadku pozytywnym (ok) zostanie odesłany obiekt przetłumaczony na XML lub JSON, a kod wynikowy to będzie 200.
// Ale w przypadku błędów możemy sami zdecydować co odsyłami (tutaj odpowiednie kody HTTP).
try(DBConnection db = DBConnection.open()) {
OrderDAO orderDAO = db.orderDAO();
Order order = orderDAO.findById(id);
return Response.ok(order).build();
} catch (DBException e) {
e.printStackTrace();
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
} catch (RecordNotFound e) {
return Response.status(Status.NOT_FOUND).build();
}
}
/*
// Metoda, która ma obsłużyć pobranie info o właścicielu zamówienia:
// /orders/1/customer
// W tej wersji metoda zwraca bezpośrednio dane klienta.
// Wada tego podejścia: ten sam rekord (konkretny klient) jest widoczny pod różnymi adresami URL.
@GET
@Path("/{id:[0-9][0-9]*}/customer")
public Customer getCustomer(@PathParam("id") Integer orderId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
OrderDAO orderDAO = db.orderDAO();
CustomerDAO customerDAO = db.customerDAO();
Order order = orderDAO.findById(orderId);
Customer customer = customerDAO.findByEmail(order.getCustomerEmail());
return customer;
}
}
*/
// W tej wersji w odpowiedzi na zapytanie o dane klienta, który złożył zamówienie,
// wyślemy przekierowanie pod adres tego klienta.
// To jest lepsze z punktu widzenia "dobrych praktyk REST".
@GET
@Path("/{id:[0-9][0-9]*}/customer")
public Response getCustomer(@PathParam("id") Integer orderId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
OrderDAO orderDAO = db.orderDAO();
Order order = orderDAO.findById(orderId);
URI customerURI = UriBuilder.fromResource(RCustomer.class)
.path("/{email}")
.build(order.getCustomerEmail());
return Response.seeOther(customerURI).build();
}
}
}
package sklep.rest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import sklep.db.DBConnection;
import sklep.db.DBException;
import sklep.db.ProductDAO;
import sklep.db.RecordNotFound;
import sklep.model.Product;
import sklep.model.ProductList;
@Path("/products.pdf")
@Produces("application/pdf")
public class RProductPDF {
@GET
public ProductList readAllProducts() throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return new ProductList(productDAO.readAll());
}
}
@Path("/{id}")
@GET
public Product readOneProduct(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return productDAO.findById(productId);
}
}
}
package sklep.rest;
import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import sklep.db.DBConnection;
import sklep.db.DBException;
import sklep.db.ProductDAO;
import sklep.db.RecordNotFound;
import sklep.model.Product;
import sklep.model.ProductList;
import sklep.photo.PhotoUtil;
/* JAX-RS - część Javy EE
* Aplikacja (projekt) składa się z wielu "resource classes", z których każda obsługuje pewien rodzaj encji
* np. pod adresem /products działa ta klasa RProduct, która zajmuje się obsługą produktów
* a pod adresem /orders działa ROrder, która zajmuje się zamówieniami.
* Przykładowa dobra (szczegółowa) dokumentacja: https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest3x/index.html
*
* W usługach REST struktura adresów URL odpowiada logicznej strukturze danych.
* Typowe jest, że * pod adresem "katalogu", np. /products , dostępna jest lista wszystkich rekordów
* (lub ogólne informacje, lub jakiś sposób przeglądania - gdyby było ich za dużo do zwrócenia na raz)
* a idąc dalej wgłąb ścieżki przechodzimy do "pojedynczych rekordów", np. /products/2
*
* W tej klasie wejdziemy nawet do wnętrza produktów i udostępnimy osobno cenę
* /products/2/price
* W praktyce nie robi się tego zbyt często, ale tu zobaczymy jako przykład możliwości.
*
* To od programisty (twórcy usługi) zależy jakie adresy i jakie operacje pod tymi adresami udostępnia.
*
* Ta klasa udostępnia (i przyjmuje) dane produktów w różnych formatach.
* Gdy w Produces jest wiele formatów, to klient może wybrać za pomocą nagłówka Accept
* Gdy w Consumes jest wiele formatów, to klient może przysłać dane w dowolnym z nich (nagłówek Content-Type)
*
*/
@Path("/products")
public class RProducts {
@GET
@Produces({"application/json", "application/xml", "text/plain", "application/pdf"})
public ProductList wszystkieProdukty() throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return new ProductList(productDAO.readAll());
}
}
// Może też być tak, że kilka metod działa pod tym samym adresem, ale służą one do tworzenia odpowiedzi w różnych formatach.
// Przykład: tworzenie HTML w oddzielnej metodzie
@GET
@Produces("text/html;charset=UTF-8")
public String wszystkieProduktyHTML() throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
List<Product> products = productDAO.readAll();
StringBuilder txt = new StringBuilder("<!DOCTYPE html>\n<html><body>\n");
txt.append("<h1>Lista produktów</h1>\n");
for(Product product : products) {
txt.append(product.toHtml()).append('\n');
}
txt.append("</body></html>");
return txt.toString();
}
}
@GET
@Path("/{id}")
@Produces({"application/json", "application/xml", "text/plain", "application/pdf"})
public Product jedenProdukt(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return productDAO.findById(productId);
}
}
@Path("/{id}")
@GET
@Produces("text/html;charset=UTF-8")
public String jedenProduktHTML(@PathParam("id") int productId) throws DBException, RecordNotFound {
Product product = jedenProdukt(productId);
return "<!DOCTYPE html>\n<html><body>" + product.toHtml() + "</body></html>";
}
@GET
@Path("/{id}/price")
@Produces({"application/json", "text/plain"})
public BigDecimal getPrice(@PathParam("id") int productId) throws DBException, RecordNotFound {
return jedenProdukt(productId).getPrice();
}
// W zapytaniach typu PUT i POST klient wysyła dane na serwer (treść zapytania: "body" / "entity" / "content" ...)
// W JAX-RS metoda może posiadać tylko jeden parametr bez adnotacji i to właśnie przez ten parametr przekazywana jest treść zapytania
// (te dane, które przysłał klient). Format tych danych opisuje adnotacja @Consumes
@PUT
@Path("/{id}/price")
@Consumes({"application/json", "text/plain"})
public void setPrice(@PathParam("id") int productId, BigDecimal newPrice) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
Product product = productDAO.findById(productId);
product.setPrice(newPrice);
productDAO.save(product);
db.commit();
}
}
// W praktyce REST metoda POST jest używana do dodawania nowych rekordów do katalogu.
// Może być także używana w innych celach - gdy klient ma "przysłać dane na serwer", a serwer coś z tym zrobi (podobnie jak to było w SOAP).
@POST
@Consumes({"application/json", "application/xml"})
@Produces({"application/json", "application/xml"})
public Response zapiszProdukt(Product product) throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
productDAO.save(product);
db.commit();
URI uri = UriBuilder.fromResource(RProducts.class).path("/{id}").build(product.getProductId());
return Response.created(uri).build();
}
}
// PUT vs POST
// PUT powinniśmy używać wtedy, gdy z góry wiadomo, pod jakim adresem zostaną zapisane dane.
// W praktyce PUT używa się najczęściej do aktualizacji istniejących danych, ale teoretycznie PUT
// można też użyć do zapisania nowych danych pod konkretnym adresem.
// Gdy wyślemy dane za pomocą PUT pod adres, to następnie GET z tego samego adresu powinien odczytać dane, które wysłał PUT (być może w innym formacie).
// POST używajmy wtedy, gdy nie wiemy z góry pod jakim adresem dane zostaną zapisane, np. gdy id rekordu jest generowane z sekwencji.
// Taka metoda powinna dać w odpowiedzi informację o ID utworzonego rekordu.
// Kilka możliwości:
// 1) (w tej klasie) - odesłanie uzupełnionego rekordu - trochę niewydajne, ale wygodne
// 2) minimalny dokumencik JSON, w którego wnętrzu jest zawarta ta informacja (tak robi wiele usług w praktyce, np. PayU)
// 3) (zobaczymy jeszcze) - odesłać odpowiedź typu Created z nagłówkiem Location - najlepsze z punktu widzenia standardów/dobrych praktyk
@DELETE
@Path("/{id}")
public void usun(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
productDAO.delete(productId);
db.commit();
}
}
@GET
@Path("/{id}/photo")
@Produces("image/jpeg")
public byte[] foto(@PathParam("id") int productId) throws DBException, RecordNotFound {
return PhotoUtil.readBytes(productId);
}
}
package sklep.rest;
import java.math.BigDecimal;
import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import sklep.db.DBConnection;
import sklep.db.DBException;
import sklep.db.ProductDAO;
import sklep.db.RecordNotFound;
import sklep.model.Product;
import sklep.photo.PhotoUtil;
@Path("/products.json")
@Consumes("application/json")
@Produces("application/json")
public class RProductsJSON {
// Wszystkie metody, które coś produkują (czyli nie są void), produkują JSON-a
// (wyjątkiem jest photo - na pozimie metody można to ustawienie zmienić)
// Wszystkie metody, które coś konsumują (czyli posiadają parametr, w którym odbierane jest "ciało zapytania")
// konsumują JSON.
@GET
public List<Product> wszystkieProdukty() throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return productDAO.readAll();
}
}
@GET
@Path("/{id}")
public Product produktWgId(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return productDAO.findById(productId);
}
}
@GET
@Path("/{id}/price")
public BigDecimal getPrice(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return productDAO.findById(productId).getPrice();
}
}
@PUT
@Path("/{id}/price")
public void setPrice(@PathParam("id") int productId, BigDecimal newPrice) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
Product product = productDAO.findById(productId);
product.setPrice(newPrice);
productDAO.save(product);
db.commit();
}
}
@POST
public Product zapiszProdukt(Product product) throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
productDAO.save(product);
db.commit();
return product;
}
}
@DELETE
@Path("/{id}")
public void usun(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
productDAO.delete(productId);
db.commit();
}
}
@GET
@Path("/{id}/photo")
@Produces("image/jpeg")
public byte[] foto(@PathParam("id") int productId) throws DBException, RecordNotFound {
return PhotoUtil.readBytes(productId);
}
}
package sklep.rest;
import java.math.BigDecimal;
import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import sklep.db.DBConnection;
import sklep.db.DBException;
import sklep.db.ProductDAO;
import sklep.db.RecordNotFound;
import sklep.model.Product;
import sklep.model.ProductList;
import sklep.photo.PhotoUtil;
@Path("/products.xml")
public class RProductsXML {
@GET
@Produces("application/xml")
public ProductList wszystkieProdukty() throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return new ProductList(productDAO.readAll());
}
}
@GET
@Path("/{id}")
@Produces("application/xml")
public Product produktWgId(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return productDAO.findById(productId);
}
}
// Pojedyncza wartość liczbowa lub tekstowa NIE JEST poprawnym (samodzielnym) XML-em
// Dlatego tu wyjątkowo użjemy text/plain.
@GET
@Path("/{id}/price")
@Produces("text/plain")
public BigDecimal getPrice(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
return productDAO.findById(productId).getPrice();
}
}
@PUT
@Path("/{id}/price")
@Consumes("text/plain")
public void setPrice(@PathParam("id") int productId, BigDecimal newPrice) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
Product product = productDAO.findById(productId);
product.setPrice(newPrice);
productDAO.save(product);
db.commit();
}
}
@POST
@Consumes("application/xml")
@Produces("application/xml")
public Product zapiszProdukt(Product product) throws DBException {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
productDAO.save(product);
db.commit();
return product;
}
}
@DELETE
@Path("/{id}")
public void usun(@PathParam("id") int productId) throws DBException, RecordNotFound {
try(DBConnection db = DBConnection.open()) {
ProductDAO productDAO = db.productDAO();
productDAO.delete(productId);
db.commit();
}
}
@GET
@Path("/{id}/photo")
@Produces("image/jpeg")
public byte[] foto(@PathParam("id") int productId) throws DBException, RecordNotFound {
return PhotoUtil.readBytes(productId);
}
}
package sklep.rest.ext;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import jakarta.servlet.ServletContext;
import jakarta.ws.rs.WebApplicationException;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.util.JAXBSource;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.xmlgraphics.util.MimeConstants;
public class ObslugaXSL {
private ServletContext servletContext;
public ObslugaXSL(ServletContext servletContext) {
this.servletContext = servletContext;
}
// obiekt -(za pomocą JAXB)-> XML -(za pomocą transformera)-> XML(XSL-FO)
// --(za pomocą Apache FOP)-> PDF -> output
public void wypiszPDF(Object obj, OutputStream output) {
try(InputStream configStream = servletContext.getResourceAsStream("WEB-INF/fop-conf.xml")) {
JAXBContext ctx = JAXBContext.newInstance(obj.getClass());
JAXBSource src = new JAXBSource(ctx, obj);
FopFactory fopFactory = FopFactory.newInstance(new URI(""), configStream);
try(BufferedOutputStream pdfOut = new BufferedOutputStream(output)) {
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, pdfOut);
TransformerFactory tf = TransformerFactory.newInstance();
StreamSource xsl = new StreamSource(servletContext.getResourceAsStream("WEB-INF/sklep-fo.xsl"));
Transformer tr = tf.newTransformer(xsl);
Result res = new SAXResult(fop.getDefaultHandler());
tr.transform(src, res);
}
} catch (Exception e) {
e.printStackTrace();
throw new WebApplicationException("Problem FOP " + e.getMessage(), e, 500);
}
}
}
package sklep.rest.ext;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import jakarta.servlet.ServletContext;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.Provider;
import sklep.model.Product;
import sklep.model.ProductList;
@Provider
public class PDFWriter implements MessageBodyWriter<Object> {
private static final MediaType PDF_TYPE = new MediaType("application", "pdf");
@Context
private ServletContext servletContext;
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return (type == ProductList.class || type == Product.class) && PDF_TYPE.isCompatible(mediaType);
}
@Override
public void writeTo(Object obj, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream output)
throws IOException, WebApplicationException {
String fileName = "products.pdf";
if (obj instanceof Product) {
Product product = (Product) obj;
fileName = product.getProductName().replace(' ', '_') + ".pdf";
}
// httpHeaders.add("Content-Disposition", "attachment;filename=" + fileName);
httpHeaders.add("Content-Disposition", "inline;filename=" + fileName);
ObslugaXSL obslugaXSL = new ObslugaXSL(servletContext);
obslugaXSL.wypiszPDF(obj, output);
}
}
package sklep.rest.ext;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import sklep.db.RecordNotFound;
@Provider
public class RecordNotFoundMapper implements ExceptionMapper<RecordNotFound> {
@Override
public Response toResponse(RecordNotFound exception) {
String html = "<html><body><h1>Nie znaleziono</h1><p style='color:red'>"
+ exception.getMessage() + "</p></body></html>";
return Response.status(404)
.type("text/html;charset=utf-8")
.entity(html)
.build();
}
}
package sklep.rest.ext;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import sklep.db.SklepException;
@Provider
public class SklepExceptionMapper implements ExceptionMapper<SklepException> {
@Override
public Response toResponse(SklepException e) {
String tresc = String.format("<html><body>"
+ "<h1>Błąd</h1>"
+ "<div>Błąd typu <code>%s</code></div>"
+ "<div style='color:red'>%s</div>"
+ "</body></html>",
e.getClass().getName(),
e.getMessage());
return Response.status(Status.INTERNAL_SERVER_ERROR)
.type("text/html;charset=utf-8")
.entity(tresc)
.build();
}
}
// a gdybyśmy chcieli mapować wszystkie wyjątki
// public class DefaultExceptionMapper implements ExceptionMapper<Exception> { ... }
url=jdbc:postgresql://localhost:5432/sklep
driver_class=org.postgresql.Driver
user=kurs
password=abc123
photo_dir=/home/patryk/sklep/foto
# Sciezki na Windows: albo piszemy slashe / , albo podwojne backslashe \\
# photo_dir=C:/Users/Patryk/Desktop/sklep/foto
# photo_dir=C:\\Users\\Patryk\\Desktop\\sklep\\foto
<?xml version="1.0"?>
<!--
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.
-->
<!-- $Id: fop.xconf 447325 2006-09-18 08:55:33Z jeremias $ -->
<!--
This is an example configuration file for FOP.
This file contains the same settings as the default values
and will have no effect if used unchanged.
Relative config url's will be resolved relative to
the location of this file.
-->
<!-- NOTE: This is the version of the configuration -->
<fop version="1.0">
<!-- Base URL for resolving relative URLs -->
<base>.</base>
<!-- Source resolution in dpi (dots/pixels per inch) for determining the size of pixels in SVG and bitmap images, default: 72dpi -->
<source-resolution>72</source-resolution>
<!-- Target resolution in dpi (dots/pixels per inch) for specifying the target resolution for generated bitmaps, default: 72dpi -->
<target-resolution>72</target-resolution>
<!-- Default page-height and page-width, in case
value is specified as auto -->
<default-page-settings height="11in" width="8.26in"/>
<!-- Information for specific renderers -->
<!-- Uses renderer mime type for renderers -->
<renderers>
<renderer mime="application/pdf">
<filterList>
<!-- provides compression using zlib flate (default is on) -->
<value>flate</value>
<!-- encodes binary data into printable ascii characters (default off)
This provides about a 4:5 expansion of data size -->
<!-- <value>ascii-85</value> -->
<!-- encodes binary data with hex representation (default off)
This filter is not recommended as it doubles the data size -->
<!-- <value>ascii-hex</value> -->
</filterList>
<fonts>
<!-- embedded fonts -->
<!--
This information must exactly match the font specified
in the fo file. Otherwise it will use a default font.
For example,
<fo:inline font-family="Arial" font-weight="bold" font-style="normal">
Arial-normal-normal font
</fo:inline>
for the font triplet specified by:
<font-triplet name="Arial" style="normal" weight="bold"/>
If you do not want to embed the font in the pdf document
then do not include the "embed-url" attribute.
The font will be needed where the document is viewed
for it to be displayed properly.
possible styles: normal | italic | oblique | backslant
possible weights: normal | bold | 100 | 200 | 300 | 400
| 500 | 600 | 700 | 800 | 900
(normal = 400, bold = 700)
-->
<!--
<font kerning="yes" embed-url="/usr/share/fonts/truetype/msttcorefonts/arial.ttf">
<font-triplet name="Arial" style="normal" weight="normal"/>
<font-triplet name="ArialMT" style="normal" weight="normal"/>
</font>
<font kerning="yes" embed-url="/usr/share/fonts/truetype/msttcorefonts/arial.ttf">
<font-triplet name="Arial" style="normal" weight="normal"/>
<font-triplet name="ArialMT" style="normal" weight="normal"/>
</font>
-->
<!-- PC -->
<directory recursive="true">/usr/share/fonts/truetype/</directory>
<directory recursive="true">C:\Windows\Fonts</directory>
</fonts>
<!-- This option lets you specify additional options on an XML handler -->
<!--xml-handler namespace="http://www.w3.org/2000/svg">
<stroke-text>false</stroke-text>
</xml-handler-->
</renderer>
<renderer mime="application/postscript">
<!-- This option forces the PS renderer to rotate landscape pages -->
<!--auto-rotate-landscape>true</auto-rotate-landscape-->
<!-- This option lets you specify additional options on an XML handler -->
<!--xml-handler namespace="http://www.w3.org/2000/svg">
<stroke-text>false</stroke-text>
</xml-handler-->
</renderer>
<renderer mime="application/vnd.hp-PCL">
</renderer>
<!-- MIF does not have a renderer
<renderer mime="application/vnd.mif">
</renderer>
-->
<renderer mime="image/svg+xml">
<format type="paginated"/>
<link value="true"/>
<strokeText value="false"/>
</renderer>
<renderer mime="application/awt">
</renderer>
<renderer mime="image/png">
<!--transparent-page-background>true</transparent-page-background-->
</renderer>
<renderer mime="image/tiff">
<!--transparent-page-background>true</transparent-page-background-->
<!--compression>CCITT T.6</compression-->
</renderer>
<renderer mime="text/xml">
</renderer>
<!-- RTF does not have a renderer
<renderer mime="text/rtf">
</renderer>
-->
<renderer mime="text/plain">
<pageSize columns="80"/>
</renderer>
</renderers>
</fop>
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output method="xml" encoding="utf-8" />
<xsl:template match="/">
<fo:root font-family="Arial">
<fo:layout-master-set>
<fo:simple-page-master master-name="A4"
page-width="210mm" page-height="297mm" margin-top="1cm"
margin-bottom="1cm" margin-left="1.5cm" margin-right="1.5cm">
<fo:region-body margin="2cm" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="A4">
<fo:flow flow-name="xsl-region-body">
<xsl:apply-templates />
<fo:block/>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
<xsl:template match="product">
<fo:block-container space-before.minimum="0.5em"
page-break-inside="avoid">
<fo:block>
<xsl:text>Produkt nr </xsl:text>
<xsl:value-of select="@id" />
<xsl:text>.</xsl:text>
</fo:block>
<fo:block-container margin="1em"
border-style="solid" border-width="2.5pt" padding="3pt"
border-color="#2233AA">
<fo:block font-weight="bold" font-size="14pt"
margin-bottom="1em" color="#FF2244">
<xsl:apply-templates select="product-name" />
</fo:block>
<fo:block font-weight="bold" color="green">
<xsl:text>Cena: </xsl:text>
<xsl:value-of select="price" />
</fo:block>
<fo:block color="green">
<xsl:text>VAT: </xsl:text>
<fo:inline font-style="italic">
<xsl:value-of select="vat * 100" />
<xsl:text>%</xsl:text>
</fo:inline>
</fo:block>
<fo:block font-size="12pt" margin-bottom="1em">
<xsl:apply-templates select="description" />
</fo:block>
</fo:block-container>
</fo:block-container>
</xsl:template>
</xsl:stylesheet>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment