Compare commits

..

No commits in common. "main" and "version2" have entirely different histories.

38 changed files with 498 additions and 1256 deletions

View File

@ -25,6 +25,13 @@
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/> <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="test" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>

View File

@ -108,25 +108,3 @@ JSF View can cause a session to be created, not just Views that
reference View- or SessionScoped backing beans. This is alterable reference View- or SessionScoped backing beans. This is alterable
by setting an option in the faces-config. by setting an option in the faces-config.
force force
### Developer/deployer note.
Because of caching, updated installations
may not render properly. Manually request the "/main.xtml" resource
and that should flush out stale info being used by "/main.jsf".
Followup: Excess payloads were being added to the welcome page
that caused HTTP "400" errors fetching resources. A fix has
been made, although the ultimate solution will probably be more
JSF-friendly.
Security note: The gourmetj password file is a simple CSV file
with "userid","password" plain text, NOT htpasswd form. Modify the
SpringSecurityConfig if you want something more rigorous.
## Minor release 2.0.1
This release can build an installable RPM for Red Hat-compatible
systems, including CentOS and its relatives and Fedora.
Just run ``mvn install`` and the RPM will be produced under the
``/target/rpm`` directory.

View File

@ -1,96 +0,0 @@
# GourmetJ application Architecture.
This is a Spring Boot self-hosted website built on standard JEE
components like JavaServer Faces (JSF) and the Java Persistence
Architecture (JPA). It is built and can be tested using Maven.
The Maven pom.xml file determines the build and what dependencies
it has. The core dependencies and their versions are determined
in the `dependencyManagement` section of the POM. Actual inclusion
of dependencies is done in the `dependencies` section.
Of the 3 dependencyManagement dependencies, the JoinFaces BOM is
the most important. It ensures that the proper version of Spring
Boot and its dependencies is brought into the `dependencies` as
well as allowing the option to select which JSF extension libraries
are to be used. For this project, PrimeFaces is the primary JSF
framework.
## Source Code components
Source code files come in 4 major types:
1. Spring/Spring Boot support
1. JSF Backing Beans
1. Data Persistence
1. General Utilities (for example data format converters and parsers)
### Spring and Spring Boot Support
The application code root package is `com.mousetech.gourmetj`. The
Spring configuration classes are located in this package. There
are two of them: `SpringPrimeFacesApplication'java`, which is the
main class for the app, and `SpringSecurityConfig.java`, which
defines security. Security credentials (userid/password) are taken
from an external password file whose location is defined by the
`application.yml` properties file. There are no security roles, and
all site pages are accessible without logging in except for the
ones that can modify the database (create/edit recipe).
Secondary Spring characteristics come from the `WelcomePageRedirect.java`
file which defines the site's welcome page (index.html).
A small bit of Spring Web in the `springweb.PicureController` class.
This Controller resolves the image URLs for recipe images and
thumbnails.
### JSF BackingBeans
The heart of the webapp is in JavaServer Faces Backing Beans which
provide the Models for their corresponding View Template (.xhtml)
files. The most important beans are the adminMain bean, which
generates the recipe list page and the recipeDetail bean, which
back the display and editing of a recipe. The CookieBean is
a utility class that allows keeping search information in user-side
cookies between user sessions and JSFUtils allows access to
HTTP/Servlet soecific resources in a way that isolates them from
the general application code. That allows platform independence
and easier unit testing.
### Persistence
Persistence is done via JPA and has 3 layers:
1. Service layer. This layer fetches and updates "working sets" of
related database entity objects. All service methods are
Transactions. Working sets passed in or out of the service layer
to the higher (business) layer of the app are detached objects,
so the Service layer also ensures detaching and re-attaching
(merging) as needed for the lower layer functions.
1. DAO layer. This layer does find/CRUD functions for a single
Entity type or sometimes a parent/child Entity set. I've coded this
using explicit logic in times past, but in this app, I've leveraged
Spring's Repository feature to let it write the grunt code.
1. Model layer. This layer contains the actual Entity classes,
which are all POJOs and have no executable logic except get/set.
### Utilities
Most utility services are in the ``utils`` package, except for
JSFUtils, which serves as a liason between JSF server-independent
code and server platform-specific services.
## Future considerations
There's probably still some lint in the POM and perhaps some dead
code from deadend solutions that weren't viable. That's how apps
are in the Real World.
Adding an I18N Bundle would probably be nice.
A Schema file was recently added to the project and it needs to
be checked for functionality. A similar `data.sql` is probably
needed to be able to fully set up a database from scratch. My
current copy was simply cloned from an existing database.

115
pom.xml
View File

@ -7,7 +7,7 @@
<groupId>com.mousetech.gourmet</groupId> <groupId>com.mousetech.gourmet</groupId>
<artifactId>gourmetj</artifactId> <artifactId>gourmetj</artifactId>
<version>2.0.37</version> <version>0.2.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>GourmetJ</name> <name>GourmetJ</name>
@ -17,7 +17,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version> <version>3.2.2</version>
<relativePath /> <!-- lookup parent from repository --> <relativePath /> <!-- lookup parent from repository -->
</parent> </parent>
@ -84,6 +84,10 @@
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- <dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
</dependency>-->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
@ -92,6 +96,14 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> </dependency>
<!--
https://mvnrepository.com/artifact/jakarta.persistence/jakarta.persistence-api -->
<!-- <dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>-->
<dependency> <dependency>
<groupId>jakarta.validation</groupId> <groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId> <artifactId>jakarta.validation-api</artifactId>
@ -117,11 +129,21 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- <dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
-->
<dependency> <dependency>
<groupId>org.apache.tomcat.embed</groupId> <groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId> <artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- <dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
</dependency>
-->
<!-- <!--
https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-core --> https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-core -->
@ -136,6 +158,7 @@
</dependency> </dependency>
<!-- <!--
https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-webp --> https://mvnrepository.com/artifact/com.twelvemonkeys.imageio/imageio-webp -->
<!-- In Core??? -->
<dependency> <dependency>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId> <artifactId>imageio-webp</artifactId>
@ -143,6 +166,7 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- Needed only if you deploy ImageIO plugins as part of a web app. <!-- Needed only if you deploy ImageIO plugins as part of a web app.
Make sure you add the IIOProviderContextListener to your web.xml.
--> -->
<dependency> <dependency>
<groupId>com.twelvemonkeys.servlet</groupId> <groupId>com.twelvemonkeys.servlet</groupId>
@ -160,6 +184,12 @@
<version>8.0.30</version> <version>8.0.30</version>
</dependency> </dependency>
<!-- <dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.faces</artifactId>
<version>${com.sun.faces.version}</version>
</dependency>-->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>
@ -172,92 +202,11 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>rpm-maven-plugin</artifactId>
<version>2.3.0</version>
<executions>
<execution>
<id>generate-rpm</id>
<goals>
<goal>rpm</goal>
</goals>
</execution>
</executions>
<configuration>
<license>GPL (c) 2024, mousetech.com</license>
<distribution>Gourmetj</distribution>
<group>Application/Collectors</group>
<packager>Tom Holloway</packager>
<prefix>/usr/local</prefix>
<changelogFile>src/changelog</changelogFile>
<defineStatements>
<defineStatement>_unpackaged_files_terminate_build 0</defineStatement>
</defineStatements>
<mappings>
<mapping>
<directory>/opt/mousetech/gourmetj</directory>
<filemode>750</filemode>
<username>gourmetj</username>
<groupname>gourmetj</groupname>
<sources>
<source>
<location>
target/${project.build.finalName}.jar</location>
</source>
<softlinkSource>
<destination>gourmetj.jar</destination>
<location>
${project.build.finalName}.jar</location>
</softlinkSource>
</sources>
</mapping>
<mapping>
<directory>/opt/mousetech/gourmetj</directory>
<configuration>true</configuration>
<filemode>640</filemode>
<username>gourmetj</username>
<groupname>gourmetj</groupname>
<sources>
<source>
<location>src/main/conf</location>
</source>
</sources>
</mapping>
<mapping>
<directory>/etc/systemd/system/</directory>
<directoryIncluded>false</directoryIncluded>
<configuration>true</configuration>
<filemode>740</filemode>
<sources>
<source>
<location>src/main/systemd/gourmetj.service</location>
</source>
</sources>
</mapping>
</mappings>
<preinstallScriptlet>
<script>echo "installing ${project.name} now"
/usr/bin/getent passwd gourmetj || /usr/sbin/useradd -r -d /opt/mousetech/gourmetj -s /sbin/nologin gourmetj
</script>
</preinstallScriptlet>
</configuration>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@ -1 +0,0 @@
# Application user password file

View File

@ -1,29 +0,0 @@
# THIS is the application properties used when testing in the IDE
# or running stand-alone from the command line.
# It augments/overrides application.yml in the JAR
joinfaces.jsf.webapp-resources-directory=/resources
server.servlet.session.timeout=30m
spring.thymeleaf.enabled=false
server.error.whitelabel.enabled=false
spring.datasource.url=jdbc:mysql://dbase/recipes
#jdbc:sqlite:${home}/recipes.db
spring.datasource.username=recipes
pring.datasource.password=yumyumyum
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
#Runtime lies and says no longer required, but it defaults to MySQL5.5.0!:
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
#org.sqlite.hibernate.dialect.SQLiteDialect
#spring.jpa.show-sql: true
# My special properties
gourmet.password.file=.gourmetpw
# This will override aplication.yml
#server.servlet.context-parameters.primefaces.THEME=le-frog
### HttpSession timeout (note effects on detailEdit idleMonitors)
server.servlet.session.timeout=35m

View File

@ -1,6 +1,7 @@
package com.mousetech.gourmetj; package com.mousetech.gourmetj;
import java.io.Serializable; import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -11,22 +12,27 @@ import com.mousetech.gourmetj.persistence.model.Recipe;
import com.mousetech.gourmetj.persistence.service.RecipeService; import com.mousetech.gourmetj.persistence.service.RecipeService;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.event.AjaxBehaviorEvent; import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.faces.model.DataModel; import jakarta.faces.model.DataModel;
import jakarta.faces.model.ListDataModel; import jakarta.faces.model.ListDataModel;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
/** /**
* Main control panel backing bean. * Main control panel backing bean.
* *
* The rare and fabled RequestScope, which is otherwise
* useless 90% of the time. Here we maintain no session
* state. so we can better support the session timeout
* for editing functions.
*
* @author timh * @author timh
* @since Jun 28, 2012 * @since Jun 28, 2012
*/ */
@Named @Named
@ViewScoped @RequestScoped
public class AdminMainBean implements Serializable { public class AdminMainBean implements Serializable {
/** /**
@ -43,6 +49,9 @@ public class AdminMainBean implements Serializable {
private static final Logger log = private static final Logger log =
LoggerFactory.getLogger(AdminMainBean.class); LoggerFactory.getLogger(AdminMainBean.class);
/** Cookie delimiter */
private static final String CKDLM = ",";
/** /**
* Persistency service for Recipes. * Persistency service for Recipes.
*/ */
@ -66,6 +75,13 @@ public class AdminMainBean implements Serializable {
return cookieBean; return cookieBean;
} }
/**
* @param cookieBean the cookieBean to set
*/
public void setCookieBean(CookieBean cookieBean) {
this.cookieBean = cookieBean;
}
// ** // **
@Inject @Inject
private UserSession userSession; private UserSession userSession;
@ -202,6 +218,13 @@ public class AdminMainBean implements Serializable {
} }
searchText = searchText.trim(); searchText = searchText.trim();
// Persist current settings
try {
cookieBean.saveCookies();
} catch (UnsupportedEncodingException e) {
// Something is really wrong if we can't create UTF-8!
log.error("Unable to save cookies!", e);
}
RecipeSearchType st = searchtypeEnum(); RecipeSearchType st = searchtypeEnum();
switch (st) { switch (st) {
case rst_BY_NAME: case rst_BY_NAME:
@ -239,7 +262,6 @@ public class AdminMainBean implements Serializable {
this.userSession.setLastEdit(null); this.userSession.setLastEdit(null);
// Construct a blank recipe to be created. // Construct a blank recipe to be created.
this.userSession.setRecipe(new Recipe()); this.userSession.setRecipe(new Recipe());
this.userSession.setDetailTab(0); // title tab
return "detailEdit?faces-redirect=true"; return "detailEdit?faces-redirect=true";
} }
@ -267,9 +289,4 @@ public class AdminMainBean implements Serializable {
// items. // items.
return "recipeDetails?faces-redirect=true"; return "recipeDetails?faces-redirect=true";
} }
public String doLogout() {
JSFUtils.logout();
return null;
}
} }

View File

@ -3,8 +3,6 @@ package com.mousetech.gourmetj;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.model.SelectItem; import jakarta.faces.model.SelectItem;
import jakarta.inject.Named; import jakarta.inject.Named;
@ -50,11 +48,4 @@ public class AppBean {
"Ingredient")); "Ingredient"));
return list; return list;
} }
@Value("${appVersion}")
String appVersion = "Not Supplied.";
public String getAppVersion() {
return appVersion;
}
} }

View File

@ -17,7 +17,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped; import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named; import jakarta.inject.Named;
/** /**
@ -28,7 +28,7 @@ import jakarta.inject.Named;
* @since Jan 31, 2024 * @since Jan 31, 2024
*/ */
@Named @Named
@ViewScoped @RequestScoped
public class CookieBean { public class CookieBean {
private static final String KEY_DISPLAY_ROWS = "displayRows"; private static final String KEY_DISPLAY_ROWS = "displayRows";
@ -44,15 +44,10 @@ public class CookieBean {
private Map<String, String> cookieMap; private Map<String, String> cookieMap;
final Map<String, Object> properties = new HashMap<>();
/** /**
* Constructor. * Constructor.
*/ */
public CookieBean() { public CookieBean() {
properties.put("maxAge", 31536000);
properties.put("path", "/");
properties.put("SameSite", "Strict");
} }
@PostConstruct @PostConstruct
@ -68,6 +63,11 @@ public class CookieBean {
*/ */
public void saveCookies() public void saveCookies()
throws UnsupportedEncodingException { throws UnsupportedEncodingException {
final Map<String, Object> properties = new HashMap<>();
properties.put("maxAge", 31536000);
properties.put("path", "/");
properties.put("SameSite", "Strict");
for (Entry<String, String> e : cookieMap.entrySet()) { for (Entry<String, String> e : cookieMap.entrySet()) {
JSFUtils.outputCookie(e.getKey(), e.getValue(), JSFUtils.outputCookie(e.getKey(), e.getValue(),
properties); properties);
@ -86,13 +86,6 @@ public class CookieBean {
public void setCookieValue(String name, String value) { public void setCookieValue(String name, String value) {
cookieMap.put(name, value); cookieMap.put(name, value);
try {
JSFUtils.outputCookie(name, value, properties);
} catch (UnsupportedEncodingException e) {
// Should never happen. But...
log.error("Unable to encode cookie", e);
e.printStackTrace();
}
} }
// ************************ // ************************
@ -104,7 +97,7 @@ public class CookieBean {
} }
public void setSearchText(String value) { public void setSearchText(String value) {
setCookieValue(KEY_SEARCH_FOR, value); cookieMap.put(KEY_SEARCH_FOR, value);
} }
// ** // **
@ -117,27 +110,24 @@ public class CookieBean {
} }
public void setSearchType(Integer value) { public void setSearchType(Integer value) {
String st = String.valueOf(value); cookieMap.put(KEY_SEARCH_TYPE, String.valueOf(value));
setCookieValue(KEY_SEARCH_TYPE, st);
} }
// ** // **
public Integer getDisplayListSize() { public Integer getDisplayListSize() {
if (!cookieMap.containsKey(KEY_DISPLAY_ROWS)) { if (!cookieMap.containsKey(KEY_DISPLAY_ROWS)) {
cookieMap.put(KEY_DISPLAY_ROWS, "0"); cookieMap.put(KEY_DISPLAY_ROWS, "30");
} }
String st = cookieMap.get(KEY_DISPLAY_ROWS); String st = cookieMap.get(KEY_DISPLAY_ROWS);
return Integer.valueOf(String.valueOf(st)); return Integer.valueOf(String.valueOf(st));
} }
public void setDisplayListSize(Integer value) { public void setDisplayListSize(Integer value) {
setCookieValue(KEY_DISPLAY_ROWS, String.valueOf(value)); cookieMap.put(KEY_DISPLAY_ROWS, String.valueOf(value));
} }
/** /**
* IdleMonitor backing methods (session/View timeout) * IdleMonitor backing methods (session/View timeout)
* Todo: move to a more general location. Currently
* only used by view editor, not Main!
*/ */
public void sessionIdleListener() { public void sessionIdleListener() {
log.info("Session Idle Listener fired."); log.info("Session Idle Listener fired.");

View File

@ -1,9 +1,8 @@
package com.mousetech.gourmetj; package com.mousetech.gourmetj;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.net.http.HttpResponse;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -13,7 +12,6 @@ import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.faces.context.Flash; import jakarta.faces.context.Flash;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -128,52 +126,29 @@ public class JSFUtils {
/** /**
* Get cookie values. * Get cookie values.
*/ */
public static Map<String, String> getCookies() { public static Map<String, String> getCookies(){
Map<String, Object> m0 = Map<String, Object> m0 = getExternalContext().getRequestCookieMap();
getExternalContext().getRequestCookieMap(); Map<String, String>m1 = new HashMap<String, String>();
Map<String, String> m1 = new HashMap<String, String>(); m1 = m0.entrySet()
m1 = m0.entrySet().stream() .stream()
.collect(Collectors.toMap(e -> e.getKey(), .collect(Collectors.toMap(
e -> ((Cookie) e.getValue()).getValue())); e -> e.getKey(),
e -> ((Cookie)e.getValue()).getValue()));
return m1; return m1;
} }
public static String getCookie(String cookieName) {
Map<String, Object> map =
getExternalContext().getRequestCookieMap();
if (map == null) {
return null; // no cookies at all
}
Cookie cookie = (Cookie) map.get(cookieName);
if (cookie == null) {
return null;
}
return cookie.getValue();
}
/** /**
* Set a cookie value in Response. * Set a cookie value in Response.
*
* @param name Cookie name * @param name Cookie name
* @param value Cookie value * @param value Cookie value
* @param properties Cookie property Map (timeout, * @param properties Cookie property Map (timeout, <i>etc.</i>)
* <i>etc.</i>)
* @throws UnsupportedEncodingException * @throws UnsupportedEncodingException
*/ */
public static void outputCookie(String name, String value, public static void outputCookie(String name,
Map<String, Object> properties) String value, Map<String, Object> properties) throws UnsupportedEncodingException {
throws UnsupportedEncodingException { getExternalContext().addResponseCookie(name,
// getExternalContext().addResponseCookie(name, URLEncoder.encode(value, "UTF-8"),
// URLEncoder.encode(value, "UTF-8"), properties);
// properties);
Cookie cookie = new Cookie(name, value);
cookie.setMaxAge(31536000);
cookie.setPath("/");
jakarta.servlet.http.HttpServletResponse resp =
(HttpServletResponse) getExternalContext()
.getResponse();
resp.addCookie(cookie);
} }
/** /**
@ -190,8 +165,4 @@ public class JSFUtils {
} }
} }
public static HttpSession getSession(boolean create) {
return (HttpSession) getExternalContext().getSession(create);
}
} }

View File

@ -14,13 +14,15 @@ import jakarta.faces.model.ListDataModel;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.servlet.http.Part;
import jakarta.faces.event.AjaxBehaviorEvent; import jakarta.faces.event.AjaxBehaviorEvent;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.primefaces.event.FileUploadEvent; import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.file.UploadedFile;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.orm.jpa.JpaSystemException;
import com.mousetech.gourmetj.persistence.model.Category; import com.mousetech.gourmetj.persistence.model.Category;
import com.mousetech.gourmetj.persistence.model.Ingredient; import com.mousetech.gourmetj.persistence.model.Ingredient;
@ -206,8 +208,8 @@ public class RecipeDetailBean implements Serializable {
/** /**
* After construction and injection, we obtain the recipe ID * After construction and injection, we obtain the recipe ID
* passed to us, if any, and load the recipe. It's also stored * passed to us, if any and load the recipe. It's also stored
* in @see UserSession for the detail editor. * in @see UserSession for the detail editor and
* *
* @see PictureController. * @see PictureController.
*/ */
@ -243,7 +245,7 @@ public class RecipeDetailBean implements Serializable {
this.shop = this.getUserSession().getShoppingList() this.shop = this.getUserSession().getShoppingList()
.contains(recipe); .contains(recipe);
log.debug("Set recipe: " + this.recipe); log.info("Set recipe: " + this.recipe);
} }
/** /**
@ -358,8 +360,8 @@ public class RecipeDetailBean implements Serializable {
return ""; return "";
} }
String s = instructions.replace("\r\n", "<p/>") String s = instructions.replace("\r\n", "<p/>")
.replace("\n\n", "<p/>") .replace("\n\n", "<p/>");
.replace("\n", "<br/>"); s = s.replace("\n", "<br/>");
return s; return s;
} }
@ -643,24 +645,15 @@ public class RecipeDetailBean implements Serializable {
* @see #addIngredientList(String) * @see #addIngredientList(String)
*/ */
public void addIngredient(String ingredientText) { public void addIngredient(String ingredientText) {
log.debug("Ingredient line: \"" + ingredientText + "\""); log.info("Ingredient line: \"" + ingredientText + "\"");
Ingredient ing = Ingredient ing =
IngredientDigester.digest(ingredientText); IngredientDigester.digest(ingredientText);
String ingkey = ing.getIngkey(); String ingkey = ing.getIngkey();
if (!StringUtils.isEmpty(ingkey)) { if (!StringUtils.isEmpty(ingkey)) {
try {
Shopcat scat = this.recipeService Shopcat scat = this.recipeService
.findShopcatForIngredientKey(ingkey); .findShopcatForIngredientKey(ingkey);
ing.setShopCat(scat); ing.setShopCat(scat);
} catch (JpaSystemException ex) {
String msg = String.format(
"Database Error: Unable to fetch info on \"%s\".",
ingkey);
log.error(msg);
JSFUtils.addErrorMessage(msg);
}
} }
// get ing list size, set ing position, append // get ing list size, set ing position, append
List<IngredientUI> ingredients = getWrappedIngredients(); List<IngredientUI> ingredients = getWrappedIngredients();
@ -720,7 +713,8 @@ public class RecipeDetailBean implements Serializable {
} }
if (recipeService.save(this.getRecipe())) { if (recipeService.save(this.getRecipe())) {
return "recipeDetails?faces-redirect=true"; userSession.setRecipe(null);
return "recipeDetails";
} else { } else {
JSFUtils.addErrorMessage("Save recipe failed"); JSFUtils.addErrorMessage("Save recipe failed");
return null; return null;
@ -977,7 +971,7 @@ public class RecipeDetailBean implements Serializable {
} }
public void ajaxUpdateShopcat(IngredientUI item) { public void ajaxUpdateShopcat(IngredientUI item) {
log.debug("SHOPCAT2 "); log.warn("SHOPCAT2 ");
updateShopcat(item); updateShopcat(item);
} }
@ -986,7 +980,7 @@ public class RecipeDetailBean implements Serializable {
public String editDescription() { public String editDescription() {
this.setDetailTab(0); this.setDetailTab(0);
return "detailEdit.xhtml?faces-redirect=true"; return "detailEdit?faces-redirect=true";
} }
public String editIngredients() { public String editIngredients() {

View File

@ -21,11 +21,9 @@ import org.springframework.http.HttpStatus;
"com.mousetech.gourmetj.persistence.model" }) "com.mousetech.gourmetj.persistence.model" })
public class SpringPrimeFacesApplication { public class SpringPrimeFacesApplication {
final String homePage = "/main.jsf?viewExpired=true";
final String errorPage = "/error/error.html"; final String errorPage = "/error/error.html";
final String error404Page = "/error/error404.jsp"; final String error404Page = "/error/error404.html";
final String error400Page = "/error/error400.jsp"; final String expiredPage = "/main.xhtml";
final String expiredPage = "/error/viewExpired.xhtml";
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SpringPrimeFacesApplication.class, SpringApplication.run(SpringPrimeFacesApplication.class,
@ -64,10 +62,8 @@ public class SpringPrimeFacesApplication {
registry.addErrorPages(new ErrorPage( registry.addErrorPages(new ErrorPage(
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
errorPage)); errorPage));
registry.addErrorPages(new ErrorPage(
HttpStatus.BAD_REQUEST,
error400Page));
} }
}; };
} }
} }

View File

@ -21,6 +21,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import jakarta.servlet.DispatcherType;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
public class SpringSecurityConfig { public class SpringSecurityConfig {
@ -69,7 +71,6 @@ public class SpringSecurityConfig {
|| (pwLine.charAt(0) == '#')) { || (pwLine.charAt(0) == '#')) {
continue; continue;
} }
//log.error("======== PWLINE="+pwLine);
String[] creds = parseCreds(pwLine); String[] creds = parseCreds(pwLine);
UserDetailsManagerConfigurer<AuthenticationManagerBuilder, UserDetailsManagerConfigurer<AuthenticationManagerBuilder,
InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder>> InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder>>
@ -99,19 +100,16 @@ public class SpringSecurityConfig {
} }
@Bean @Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
throws Exception {
http.csrf(AbstractHttpConfigurer::disable) http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable) .cors(AbstractHttpConfigurer::disable)
.formLogin(login -> login.loginPage("/login.jsf") .formLogin(Customizer.withDefaults())
.permitAll() .authorizeHttpRequests((authorize)-> authorize
.failureUrl("/login.jsf?error=true")) .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.logout(logout -> logout .anyRequest().authenticated()
.logoutSuccessUrl("/login.jsf")) );
.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated());
return http.build(); return http.build();
} }
@ -123,19 +121,13 @@ public class SpringSecurityConfig {
@Bean @Bean
public WebSecurityCustomizer webSecurityCustomizer() { public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers( return (web) -> web.ignoring().requestMatchers(
"/jakarta.faces.resource/**", "/javax.faces.resource/**",
"/", "/",
"/index.html", "/index.jsf",
// "/login", "/index.xhtml",
// "/login.jsf", // Leave them for the authenticator!
// "/login.xhtml",
"/main.jsf", "/main.jsf",
"/main.xhtml",
"/img/**", "/img/**",
"/error/**",
"/RES_NOT_FOUND",
"/recipeDetails.jsf", "/recipeDetails.jsf",
"/recipeDetails.xhtml",
"/shoppingList.jsf", "/shoppingList.jsf",
"/recipePrint.jsf"); "/recipePrint.jsf");
} }

View File

@ -1,16 +1,14 @@
package com.mousetech.gourmetj; package com.mousetech.gourmetj;
import java.io.Serializable; import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import jakarta.enterprise.context.SessionScoped; import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.model.SelectItem;
import jakarta.inject.Named; import jakarta.inject.Named;
import org.primefaces.PrimeFaces;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -148,54 +146,6 @@ public class UserSession implements Serializable {
return sb.toString(); return sb.toString();
} }
/**
* Display source. If no "source" and there's a URL,
* use that.
*
* @param ltime
* @return
*/
public String formatSource(Recipe r) {
String s = r.getSource();
if ( s != null && ! s.isBlank()) {
return s;
}
s = r.getLink();
return urlToSource(s);
}
/**
* Take a URL and strip it of everything but
* the base domain name (including ".com")
* @param s URL string.
* @return Processed domain name.
* TestedBy: UserSessionTest
*/
String urlToSource(String s) {
if ( s == null || s.isBlank()) {
return ""; // no source, no URL
}
try {
if ( ! s.startsWith("http")) {
s = "http://" + s; // Convert to absolute URI
}
URL u = new URI(s).toURL();
String s1 = u.getHost();
if ( s1.startsWith("www.")) {
s1 = s1.substring("www.".length());
}
if ( s1.endsWith(".com")) {
// mousetech.com
s1 = s1.substring(0, s1.length() - ".com".length());
}
return s1;
} catch (MalformedURLException e) {
return s;
} catch (URISyntaxException e) {
return s;
}
}
/* /*
* @Deprecated Using TimeConverter. * @Deprecated Using TimeConverter.
*/ */

View File

@ -11,7 +11,7 @@ public class WelcomePageRedirect implements WebMvcConfigurer {
@Override @Override
public void addViewControllers(ViewControllerRegistry registry) { public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/") registry.addViewController("/")
.setViewName("forward:/index.html"); .setViewName("forward:/index.xhtml");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE); registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
} }
} }

View File

@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.mousetech.gourmetj.persistence.model.Category;
import com.mousetech.gourmetj.persistence.model.Recipe; import com.mousetech.gourmetj.persistence.model.Recipe;
/** /**
@ -23,7 +24,7 @@ import com.mousetech.gourmetj.persistence.model.Recipe;
public interface RecipeRepository public interface RecipeRepository
extends JpaRepository<Recipe, Long> { extends JpaRepository<Recipe, Long> {
List<Recipe> findByTitleContainingIgnoreCase(String searchText); List<Recipe> findByTitleContaining(String searchText);
@EntityGraph(value="Recipe.findWorkingSet") @EntityGraph(value="Recipe.findWorkingSet")
public Recipe findDetailsById(Long recipeId); public Recipe findDetailsById(Long recipeId);
@ -31,11 +32,11 @@ public interface RecipeRepository
@Query(name = "Recipe.findCusines", nativeQuery = true) @Query(name = "Recipe.findCusines", nativeQuery = true)
List<String> FindCuisinesNative(); List<String> FindCuisinesNative();
List<Recipe> findByCategories_CategoryContainsIgnoreCase(String searchText); List<Recipe> findByCategories_CategoryContains(String searchText);
List<Recipe> findByCuisineContainsIgnoreCase(String searchText); List<Recipe> findByCuisineContains(String searchText);
List<Recipe> findDistinctByIngredientHash_ItemContainsIgnoreCase( List<Recipe> findDistinctByIngredientHash_ItemContains(
String searchText); String searchText);
} }

View File

@ -50,7 +50,7 @@ public class RecipeService implements Serializable {
public List<Recipe> findByTitle(String searchText) { public List<Recipe> findByTitle(String searchText) {
return recipeRepository return recipeRepository
.findByTitleContainingIgnoreCase(searchText); .findByTitleContaining(searchText);
} }
public Recipe findByPrimaryKey(Long recipeId) { public Recipe findByPrimaryKey(Long recipeId) {
@ -120,19 +120,14 @@ public class RecipeService implements Serializable {
} }
public List<Recipe> findByCategoryLike(String searchText) { public List<Recipe> findByCategoryLike(String searchText) {
return recipeRepository return recipeRepository.findByCategories_CategoryContains(searchText);
.findByCategories_CategoryContainsIgnoreCase(
searchText);
} }
public List<Recipe> findByCuisineLike(String searchText) { public List<Recipe> findByCuisineLike(String searchText) {
return recipeRepository return recipeRepository.findByCuisineContains(searchText);
.findByCuisineContainsIgnoreCase(searchText);
} }
public List<Recipe> findByIngredientLike(String searchText) { public List<Recipe> findByIngredientLike(String searchText) {
return recipeRepository return recipeRepository.findDistinctByIngredientHash_ItemContains(searchText);
.findDistinctByIngredientHash_ItemContainsIgnoreCase(
searchText);
} }
} }

View File

@ -17,6 +17,9 @@ import java.io.InputStream;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.Part;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -34,9 +37,6 @@ import com.mousetech.gourmetj.UserSession;
import com.mousetech.gourmetj.persistence.model.Recipe; import com.mousetech.gourmetj.persistence.model.Recipe;
import com.mousetech.gourmetj.persistence.service.RecipeService; import com.mousetech.gourmetj.persistence.service.RecipeService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
/** /**
* @author timh * @author timh
* @since Nov 26, 2021 * @since Nov 26, 2021

View File

@ -25,7 +25,7 @@
<navigation-case> <navigation-case>
<description>Go Home</description> <description>Go Home</description>
<from-outcome>home</from-outcome> <from-outcome>home</from-outcome>
<to-view-id>/main.xhtml?faces-redirect=true</to-view-id> <to-view-id>/main</to-view-id>
<redirect /> <redirect />
</navigation-case> </navigation-case>
</navigation-rule> </navigation-rule>

View File

@ -18,7 +18,7 @@
</h:head> </h:head>
<h:body> <h:body>
<h1> <h1>
<ui:insert name="title">Gourmet Recipe Manager (web version #{appBean.appVersion})</ui:insert> <ui:insert name="title">Gourmet Recipe Manager (web version)</ui:insert>
</h1> </h1>
<p:ajaxStatus onerror="PF('opError').show()"/> <p:ajaxStatus onerror="PF('opError').show()"/>
<ui:insert name="content"> <ui:insert name="content">
@ -28,11 +28,11 @@
</ui:insert> </ui:insert>
<!-- --> <!-- -->
<div id="footer"> <div id="footer">
<h:outputText value="Version #{appBean.appVersion}"/><br/> (C) 2021, 2024 Tim Holloway, Licensed under the <a
(C) 2021, 2024 Tim Holloway, Licensed under href="http://www.apache.org/licenses/LICENSE-2.0"
the Common Development and Distribution License (CDDL). >Apache License, Version 2.0</a>.
<p>Based on Gourmet Recipe Manager by T. <p>Based on Gourmet Recipe Manager by T.
Hinkle.</p> Hinkle</p>
</div> </div>
<!-- --> <!-- -->
</h:body> </h:body>

View File

@ -1,163 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<ui:composition
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jstl"
>
<!-- Overview tab body -->
<p:panelGrid columns="2"
columnClasses="deDescl, deDescr"
>
<f:facet name="header">Description</f:facet>
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="Title" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:inputText id="rtitle" size="45"
required="true"
placeholder="A recipe title is required."
value="#{recipeDetailBean.recipe.title}"
>
<p:focus />
<f:ajax execute="rtitle"
render="editorPanel" />
</p:inputText>
</p:column>
</p:row>
<!-- -->
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="Category" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:inputText id="rcategory"
label="Category"
value="#{recipeDetailBean.category}"
tip="One or more categories, separated by commas (ex: Entree, Soup)" />
<p:commandButton
value="&lt;- Suggest"
>
<f:ajax execute="rcategory bxCat"
listener="#{recipeDetailBean.ajaxSuggestCategory}"
render="rcategory" />
</p:commandButton>
<p:selectOneMenu id="bxCat"
value="#{recipeDetailBean.catToAdd}"
tip="Recipe category suggestions, based on previous selections"
>
<f:selectItems
value="#{recipeDetailBean.suggestCategory}" />
</p:selectOneMenu>
</p:column>
</p:row>
<!-- -->
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="Cuisine" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:autoComplete id="rcuisine"
value="#{recipeDetailBean.recipe.cuisine}"
completeMethod="#{recipeDetailBean.cuisineSuggestions}" />
</p:column>
</p:row>
<p:row>
<p:column styleClass="deDescr" colspan="3">
<p:outputLabel value="Prep Time" />
<p:inputText id="rpreptime" max="10"
value="#{recipeDetailBean.recipe.preptime}"
>
<f:converter
converterId="com.mousetech.gourmetj.utils.TimeConverter" />
</p:inputText>
</p:column>
</p:row>
<!-- -->
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="Cooking Time" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:inputText id="rcooktime" max="10"
value="#{recipeDetailBean.recipe.cooktime}"
>
<f:converter
converterId="com.mousetech.gourmetj.utils.TimeConverter" />
</p:inputText>
</p:column>
</p:row>
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="Servings" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:inputText id="rserves" max="10"
value="#{recipeDetailBean.recipe.servings}" />
</p:column>
</p:row>
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="Rating" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:rating id="rrating" max="10"
value="#{recipeDetailBean.recipe.rating}" />
</p:column>
</p:row>
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="Source" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:inputText id="rsource" size="45"
value="#{recipeDetailBean.recipe.source}" />
</p:column>
</p:row>
<p:row>
<p:column styleClass="deDescl">
<p:outputLabel value="URL" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:inputText id="rurl" size="45"
value="#{recipeDetailBean.recipe.link}" />
</p:column>
</p:row>
<p:row>
<p:column styleClass="deDescl" colspan="3">
<p:outputLabel value="Description" />
</p:column>
<p:column styleClass="deDescr" colspan="3">
<p:inputTextarea id="description"
rows="10" cols="45"
value="#{recipeDetailBean.recipe.description}" />
</p:column>
</p:row>
</p:panelGrid>
<p:panelGrid id="ppGrid" columns="2">
<p:panel id="picPanel">
<img id="bigPix"
src="/img/picture/?dt=#{recipeDetailBean.currentTime}" />
</p:panel>
<p:panelGrid id="picButtonPanel" columns="1">
<p:fileUpload id="ctlUpload"
label="Upload Image"
listener="#{recipeDetailBean.ajaxUploadImage}"
global="true" mode="advanced" multiple="false"
update="picPanel" auto="true" sizeLimit="1000000"
allowTypes="/(\.|\/)(gif|jpe?g|png|webp)$/" />
<p:commandButton id="ctlDelImg"
value="Delete Image"
action="#{recipeDetailBean.ajaxDeleteImage}"
update="picPanel" immediate="true" />
</p:panelGrid>
</p:panelGrid>
</ui:composition>

View File

@ -42,10 +42,6 @@
font-weight: bold; font-weight: bold;
background-color: green; background-color: green;
} }
.noBorders .noBorders tr, .noBorders td {
background: none !important;
border: none !important;
}
</style> </style>
<h:messages id="messages" /> <h:messages id="messages" />
<p:panel id="editorPanel" <p:panel id="editorPanel"
@ -59,8 +55,129 @@
<p:tab id="overviewTab" <p:tab id="overviewTab"
title="Description" title="Description"
> >
<ui:include <p:panelGrid columns="2"
src="/WEB-INF/layout/panels/editRecipe/overview.xhtml"/> columnClasses="deDescl, deDescr"
>
<f:facet name="header">Description</f:facet>
<p:outputLabel for="@next"
value="Title"
/>
<p:inputText id="rtitle"
size="45" required="true"
focus="true"
placeholder="A recipe title is required."
value="#{recipeDetailBean.recipe.title}"
>
<f:ajax execute="rtitle"
render="editorPanel"
/>
</p:inputText>
<p:outputLabel for="@next"
value="Category"
/>
<p:inputText id="rcategory"
label="Category"
value="#{recipeDetailBean.category}"
tip="One or more categories, separated by commas (ex: Entree, Soup)"
/>
<p:commandButton
value="&lt;- Suggest"
>
<f:ajax
execute="rcategory bxCat"
listener="#{recipeDetailBean.ajaxSuggestCategory}"
render="rcategory"
/>
</p:commandButton>
<p:selectOneMenu id="bxCat"
value="#{recipeDetailBean.catToAdd}"
tip="Recipe category suggestions, based on previous selections"
>
<f:selectItems
value="#{recipeDetailBean.suggestCategory}"
/>
</p:selectOneMenu>
<p:outputLabel for="@next"
value="Cuisine"
/>
<p:autoComplete id="rcuisine"
value="#{recipeDetailBean.recipe.cuisine}"
completeMethod="#{recipeDetailBean.cuisineSuggestions}"
/>
<p:outputLabel for="@next"
value="Prep Time"
/>
<p:inputText id="rpreptime"
max="10"
value="#{recipeDetailBean.recipe.preptime}"
>
<f:converter
converterId="com.mousetech.gourmetj.utils.TimeConverter"
/>
</p:inputText>
<p:outputLabel for="@next"
value="Cooking Time"
/>
<p:inputText id="rcooktime"
max="10"
value="#{recipeDetailBean.recipe.cooktime}"
>
<f:converter
converterId="com.mousetech.gourmetj.utils.TimeConverter"
/>
</p:inputText>
<p:outputLabel for="@next"
value="Rating"
/>
<p:rating id="rrating" max="10"
value="#{recipeDetailBean.recipe.rating}"
/>
<p:outputLabel for="@next"
value="Source"
/>
<p:inputText id="rsource"
size="45"
value="#{recipeDetailBean.recipe.source}"
/>
<p:outputLabel for="@next"
value="URL"
/>
<p:inputText id="rurl" size="45"
value="#{recipeDetailBean.recipe.link}"
/>
<p:outputLabel for="@next"
value="Description"
/>
<p:inputTextarea id="description"
rows="10" cols="45"
value="#{recipeDetailBean.recipe.description}"
/>
</p:panelGrid>
<p:panel id="picPanel">
<img id="bigPix"
src="/img/picture/?dt=#{recipeDetailBean.currentTime}"
/>
</p:panel>
<p:panelGrid id="picButtonPanel"
columns="2"
>
<p:fileUpload id="ctlUpload"
label="Upload Image"
listener="#{recipeDetailBean.ajaxUploadImage}"
global="true" mode="advanced"
multiple="false"
update=":messages picPanel"
auto="true"
sizeLimit="1000000"
allowTypes="/(\.|\/)(gif|jpe?g|png|webp)$/"
/>
<p:commandButton id="ctlDelImg"
value="Delete Image"
action="#{recipeDetailBean.ajaxDeleteImage}"
update="picPanel"
immediate="true"
/>
</p:panelGrid>
</p:tab> </p:tab>
<p:tab id="ingredientsTab" <p:tab id="ingredientsTab"
title="Ingredients" title="Ingredients"
@ -245,8 +362,8 @@
id="ctlAddIng" id="ctlAddIng"
value="+ Add" value="+ Add"
onclick="ingButton(); return false;" onclick="ingButton(); return false;"
update=":growl" >
/> </p:commandButton>
</h:panelGroup> </h:panelGroup>
</p:panel> </p:panel>
</p:tab> </p:tab>
@ -260,9 +377,7 @@
rows="30" cols="120" rows="30" cols="120"
escape="false" escape="false"
value="#{recipeDetailBean.recipe.instructions}" value="#{recipeDetailBean.recipe.instructions}"
> />
<p:focus />
</h:inputTextarea>
</div> </div>
</p:panel> </p:panel>
</p:tab> </p:tab>
@ -272,24 +387,18 @@
rows="30" cols="120" rows="30" cols="120"
escape="false" escape="false"
value="#{recipeDetailBean.recipe.modifications}" value="#{recipeDetailBean.recipe.modifications}"
> />
<p:focus/>
</h:inputTextarea>
</p:panel> </p:panel>
</p:tab> </p:tab>
</p:tabView> </p:tabView>
<p:commandButton id="doSave" value="Save" <p:commandButton id="doSave" value="Save" icon="ui-icon-pencil" ajax="false" disabled="{not recipeDetailBean.dirty}" action="#{recipeDetailBean.doSave}" />
icon="ui-icon-pencil" ajax="false"
disabled="{not recipeDetailBean.dirty}"
action="#{recipeDetailBean.doSave}"
/>
<p:commandButton id="doCancel" value="Cancel" <p:commandButton id="doCancel" value="Cancel"
ajax="false" immediate="true" ajax="false" immediate="true"
action="recipeDetails.jsf" action="recipeDetails.jsf"
/> />
<p:commandButton id="doHome" value="Home" <p:commandButton id="doHome" value="Home"
icon="ui-icon-home" ajax="false" icon="ui-icon-home" ajax="false"
immediate="true" action="home" immediate="true" action="main.jsf"
/> />
</h:form> </h:form>
</p:panel> </p:panel>

View File

@ -1,12 +0,0 @@
<%@ page language="java" contentType="text/html; charset=US-ASCII"
pageEncoding="US-ASCII" isErrorPage="true"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=US-ASCII">
<title>Error 400 Page - Bad request</title>
</head>
<body>
<font color="red">Error: exception.getMessage() </font><br>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<html>
<head>
<title>ERROR - Page Not Found</title>
</head>
<body>
<h1>Page Not Found</h1>
<p>This URL is invalid.</p>
<p><a href="/main.jsf">Return to Main Page</a></p>
</body>
</html>

View File

@ -1,12 +0,0 @@
<%@ page language="java" contentType="text/html; charset=US-ASCII"
pageEncoding="US-ASCII" isErrorPage="true"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ERROR - Page Not Found</title>
</head>
<body>
<h1>Page Not Found</h1>
<p><a href="/main.jsf">Return to Main Page</a></p>
</body>
</html>

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ERROR - Page Expired</title>
</head>
<body>
<h1>Page Expired</h1>
<p>The page state could not be restored because it was
left idle too long.</p>
<p>
<a href="/main.jsf">Return to Main Page</a>
</p>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html>
<head><title>Gourmet Recipe Manager</title><head>
<body>
<h1>Gourmet Recipe Manager</h1>
<p>This is an implementation of Thomas Hinkle's
Gourmet Recipe Manager, originally a desktop
application but now available as a Java Web
application.</p>
<p><a href="main.jsf">
Go to Main Page</a></p>
<hr/>
<p>Copyright © 2021, 2024 Tim Holloway. All Rights Reserved.
<p>This is an open-source application under the
Common Development and Distribution License (CDDL).
</p>
<body>
</html>

View File

@ -0,0 +1,26 @@
<?xml version="1.0"?>
<ui:composition template="/WEB-INF/layout/layout.xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jstl"
>
<!-- Print Recipe -->
<ui:define name="title">Gourmet Recipe Manager</ui:define>
<ui:define name="content">
<h:form id="printForm">
<h:messages />
<p>This is an implementation of Thomas Hinkle's
Gourmet Recipe Manager, originally a desktop
application but now available as a Java Web
application.</p>
<h:outputLink
value="/main.jsf"
>Go to Main Page</h:outputLink>
</h:form>
<div style="height: 20px">
<h:outputText value="" />
</div>
</ui:define>
</ui:composition>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:p="http://primefaces.org/ui"
xmlns:pe="http://primefaces.org/ui/extensions"
>
<h:head>
<title>Login</title>
</h:head>
<h:body>
<h:form prependId="false" style="width:100%">
<p:panelGrid columns="3" style="width:100%"
styleClass="ui-fluid center ui-noborder"
>
<h:outputText style="width:33%;" value=" " />
<p:panelGrid columns="1" id="grid1">
<h2>Please login</h2>
<p:outputLabel value="Login failed!"
styleClass="red"
rendered="${!empty param['error']}"
/>
<p:outputLabel for="username">User ID</p:outputLabel>
<p:inputText id="username"
placeholder="User name"
/>
<p:outputLabel for="password">Password</p:outputLabel>
<p:password id="password" placeholder="Password" />
<p:commandButton value="Login" ajax="false" />
</p:panelGrid>
<h:outputText style="width:33%;" value=" " />
</p:panelGrid>
</h:form>
</h:body>
</html>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html >
<ui:composition template="/WEB-INF/layout/layout.xhtml" <ui:composition template="/WEB-INF/layout/layout.xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
@ -11,22 +10,25 @@
<h:messages /> <h:messages />
<h:form id="form1"> <h:form id="form1">
<div> <div>
<p:focus />
<p:autoComplete id="searchFor" size="45" <p:autoComplete id="searchFor" size="45"
placeholder="Recipe title/cuisine/category, etc.)" placeholder="Recipe title/cuisine/category, etc.)"
value="#{adminMainBean.searchText}" value="#{adminMainBean.searchText}"
completeMethod="#{adminMainBean.searchSuggestionList}" completeMethod="#{adminMainBean.searchSuggestionList}"
onfocus="jQuery('#form1\\:searchFor_input').select();"
> >
<f:ajax event="change" execute="@this" <f:ajax event="change" execute="@this"
render="form2:table1" render="form2:table1"
listener="#{adminMainBean.ajaxUpdateList}" listener="#{adminMainBean.ajaxUpdateList}"
/> />
</p:autoComplete> </p:autoComplete>
<p:outputLabel for="@next" value=" In " /> <p:defaultCommand target="find" />
<p:commandButton id="find" value="Find"
icon="ui-icon-search"
action="#{adminMainBean.doFind}"
update=":form2:table1"
/>
<p:outputLabel for="@next" value="Search for " />
<p:selectOneMenu id="ctlSearchType" <p:selectOneMenu id="ctlSearchType"
value="#{cookieBean.searchType}" value="#{cookieBean.searchType}"
onchange="jQuery('#form1\\:searchFor_input').trigger('focus');"
> >
<f:selectItems <f:selectItems
value="#{appBean.searchTypeList}" value="#{appBean.searchTypeList}"
@ -35,12 +37,6 @@
listener="#{adminMainBean.resetSuggestions}" listener="#{adminMainBean.resetSuggestions}"
/> />
</p:selectOneMenu> </p:selectOneMenu>
<p:defaultCommand target="find" />
<p:commandButton id="find" value="Find"
icon="ui-icon-search"
action="#{adminMainBean.doFind}"
update=":form2:table1"
/>
<p:commandButton id="ctlClear" value="Clear" <p:commandButton id="ctlClear" value="Clear"
icon="ui-icon-close" icon="ui-icon-close"
update="@form:searchFor :form2:table1" update="@form:searchFor :form2:table1"
@ -57,10 +53,7 @@
value="#{cookieBean.displayListSize}" value="#{cookieBean.displayListSize}"
/> />
<h:outputLabel for="slistSize" <h:outputLabel for="slistSize"
value=" Recipes in Shopping List " value=" Recipes in Shopping List"
/>
<p:commandButton id="logout" value="Logout"
action="#{adminMainBean.doLogout}"
/> />
</div> </div>
</h:form> </h:form>
@ -98,7 +91,7 @@
/> />
</p:column> </p:column>
<p:column headerText="Source"> <p:column headerText="Source">
<h:outputText value="#{userSession.formatSource(row)}" /> <h:outputText value="#{row.source}" />
</p:column> </p:column>
<p:column headerText="Prep Time"> <p:column headerText="Prep Time">
<h:outputText value="#{row.preptime}" <h:outputText value="#{row.preptime}"

View File

@ -35,98 +35,71 @@
/> />
</f:facet> </f:facet>
<p:panel id="leftCol" style="width: auto;"> <p:panel id="leftCol" style="width: auto;">
<p:panelGrid> <p:panelGrid columns="2">
<p:row> <p:panelGrid id="picButtons" columns="2">
<p:column style="width: 136px;"> <img id="bigpix"
<p:graphicImage id="bigpix"
style="width: 132px;" style="width: 132px;"
value="/img/picture/#{recipeDetailBean.recipe.id}" src="/img/picture/#{recipeDetailBean.recipe.id}"
/> />
</p:column>
<p:column style="width: 18em;">
<p:panelGrid id="pnlButtons" <p:panelGrid id="pnlButtons"
columns="2" columns="2" style="width: 220px;"
> >
<!-- TODO: ask if we should save --> <!-- TODO: ask if we should save -->
<p:commandButton value="Home" <p:commandButton value="Back"
ajax="false" ajax="false"
icon="ui-icon-home" icon="ui-icon-arrowthick-1-w"
action="home" action="home"
immediate="true" immediate="true"
/> />
<p:commandButton ajax="false" <p:commandButton ajax="false"
value="Print" value="Print"
icon="ui-icon-print" icon="ui-icon-print"
action="/recipePrint.jsf?faces-redirect=true" action="recipePrint.jsf"
styleClass="ui-button-print" styleClass="ui-button-print"
immediate="true" immediate="true"
/> />
<p:commandButton id="ctlShop" <p:commandButton id="ctlShop"
icon="ui-icon-cart" icon="ui-icon-cart"
value="Shop" value="Shop" immediate="true"
immediate="true"
styleClass="#{recipeDetailBean.shop ? 'greenButton' : null}" styleClass="#{recipeDetailBean.shop ? 'greenButton' : null}"
action="#{recipeDetailBean.doShop}" action="#{recipeDetailBean.doShop}"
update="ctlShop" update="ctlShop"
/> />
<p:commandButton <h:outputText value="" />
icon="ui-icon-wrench"
value="Edit"
action="#{recipeDetailBean.editDescription}"
/>
<p:outputLabel for="@next" <p:outputLabel for="@next"
value="Categories:" value="Categories:"
/> />
<h:outputText <h:outputText label="Category: "
label="Category: "
value="#{userSession.formatCategories(recipeDetailBean.recipe)}" value="#{userSession.formatCategories(recipeDetailBean.recipe)}"
/> />
<p:outputLabel for="@next" <p:outputLabel for="@next"
value="Cuisine:" value="Cuisine:"
/> />
<h:outputText <h:outputText label="Cuisine: "
label="Cuisine: "
value="#{recipeDetailBean.recipe.cuisine}" value="#{recipeDetailBean.recipe.cuisine}"
/> />
<p:outputLabel for="@next" <p:outputLabel for="@next"
value="Prep Time:" value="Prep Time:"
/> />
<h:outputText <h:outputText label="Prep Time: "
label="Prep Time: "
value="#{recipeDetailBean.recipe.preptime}" value="#{recipeDetailBean.recipe.preptime}"
converter="com.mousetech.gourmetj.utils.TimeConverter" converter="com.mousetech.gourmetj.utils.TimeConverter"
/> />
<p:outputLabel for="@next" <p:outputLabel for="@next"
value="Cook Time:" value="Cook Time:"
/> />
<h:outputText <h:outputText label="Cook Time: "
label="Cook Time: "
value="#{recipeDetailBean.recipe.cooktime}" value="#{recipeDetailBean.recipe.cooktime}"
converter="com.mousetech.gourmetj.utils.TimeConverter" converter="com.mousetech.gourmetj.utils.TimeConverter"
/> />
<p:outputLabel for="@next" <h:outputText value="" />
value="Servings:" <p:commandButton
/> icon="ui-icon-wrench"
<h:outputText value="Edit"
value="#{recipeDetailBean.recipe.servings}" action="#{recipeDetailBean.editDescription}"
/> />
</p:panelGrid> </p:panelGrid>
</p:column> </p:panelGrid>
<p:column>
<p:panel id="sources" style="width: fit-content;">
<h:outputText
value="#{recipeDetailBean.recipe.source}"
/>
<br />
<h:outputLink
value="#{recipeDetailBean.recipe.link}"
rendered="#{not empty recipeDetailBean.recipe.link}"
>
#{recipeDetailBean.recipe.link}
</h:outputLink>
</p:panel>
</p:column>
</p:row>
</p:panelGrid> </p:panelGrid>
<!-- --> <!-- -->
<p:panel id="pnlInstr"> <p:panel id="pnlInstr">

View File

@ -9,51 +9,48 @@
<!-- Print Recipe --> <!-- Print Recipe -->
<ui:define name="title">Gourmet Recipe Manager</ui:define> <ui:define name="title">Gourmet Recipe Manager</ui:define>
<ui:define name="content"> <ui:define name="content">
<h:form id="printForm" style="font-size: 12pt;"> <h:form id="printForm">
<h:messages /> <h:messages />
<p:commandButton value="Back" immediate="true" <p:panelGrid
ajax="false" icon="ui-icon-arrowthick-1-w" style="margin-bottom: 5px; border-style: none"
style="margin-left: 2em" styleClass="noprint"
action="recipeDetails.jsf?faces-redirect=true"
/>
<p:panelGrid id="pTopGrid"
columns="2" style="margin-bottom: 5px; border-style: none"
> >
<p:column width="50%"> <p:column>
<img id="bigpix" <img id="bigpix"
SRC="/img/picture/#{recipeDetailBean.recipe.id}" SRC="/img/picture/#{recipeDetailBean.recipe.id}"
/> />
<p:panelGrid columns="2" id="pGrid2"> </p:column>
<h:outputLabel for="@next" <p:column
value="Category: " style="vertical-align: middle; text-align: left; border: none"
>
<p:commandButton value="Back"
immediate="true" ajax="false"
icon="ui-icon-arrowthick-1-w"
style="margin-left: 2em"
styleClass="noprint"
action="recipeDetails.jsf?faces-redirect=true"
/> />
</p:column>
</p:panelGrid>
<p:panelGrid columns="2">
<h:outputLabel for="@next" value="Category: " />
<h:outputText <h:outputText
value="#{userSession.formatCategories(recipeDetailBean.recipe)}" value="#{userSession.formatCategories(recipeDetailBean.recipe)}"
/> />
<h:outputLabel for="@next" <h:outputLabel for="@next" value="Cuisine: " />
value="Cuisine: "
/>
<h:outputText <h:outputText
value="#{recipeDetailBean.recipe.cuisine}" value="#{recipeDetailBean.recipe.cuisine}"
/> />
<h:outputLabel for="@next" <h:outputLabel for="@next" value="Prep Time: " />
value="Prep Time: "
/>
<h:outputText <h:outputText
value="#{userSession.formatTime(recipeDetailBean.recipe.preptime)}" value="#{userSession.formatTime(recipeDetailBean.recipe.preptime)}"
/> />
<h:outputLabel for="@next" <h:outputLabel for="@next" value="Cook Time: " />
value="Cook Time: "
/>
<h:outputText label="Cook Time: " <h:outputText label="Cook Time: "
value="#{userSession.formatTime(recipeDetailBean.recipe.cooktime)}" value="#{userSession.formatTime(recipeDetailBean.recipe.cooktime)}"
/> />
</p:panelGrid> </p:panelGrid>
</p:column> <!-- -->
<p:column <p:panelGrid id="ingredientsc">
width="50%" style="vertical-align: middle; text-align: left; border: none"
>
<p:panelGrid id="ingredientsc" columns="1">
<f:facet name="header"> <f:facet name="header">
<h:outputText styleClass="subtitle" <h:outputText styleClass="subtitle"
value="Ingredients" value="Ingredients"
@ -64,7 +61,6 @@
showDirectLinksArrows="true" showDirectLinksArrows="true"
value="#{recipeDetailBean.ingredients}" value="#{recipeDetailBean.ingredients}"
var="ingredient" var="ingredient"
style="width: 100%; font-size: 12pt;"
> >
<p:column <p:column
style="text-align: right; width: 2em" style="text-align: right; width: 2em"
@ -104,11 +100,10 @@
</p:dataTable> </p:dataTable>
</p:column> </p:column>
</p:panelGrid> </p:panelGrid>
</p:column> <h:outputText
</p:panelGrid> value="Recipe ID: #{recipeDetailBean.recipe.id}"
<!-- --> />
<!-- --> <p:panelGrid columns="1" style="width: 100%">
<p:panelGrid columns="1" style="width: 100%; font-size: 12pt;">
<f:facet name="header"> <f:facet name="header">
<h:outputText styleClass="subtitle" <h:outputText styleClass="subtitle"
value="Instructions" value="Instructions"
@ -119,7 +114,7 @@
value="#{recipeDetailBean.instructions}" value="#{recipeDetailBean.instructions}"
/> />
</p:panelGrid> </p:panelGrid>
<p:panelGrid columns="1" style="width: 100%; font-size: 12pt" <p:panelGrid columns="1" style="width: 100%"
rendered="#{not empty recipeDetailBean.modifications}" rendered="#{not empty recipeDetailBean.modifications}"
> >
<f:facet name="header"> <f:facet name="header">
@ -131,9 +126,6 @@
value="#{recipeDetailBean.modifications}" value="#{recipeDetailBean.modifications}"
/> />
</p:panelGrid> </p:panelGrid>
<h:outputText
value="Recipe ID: #{recipeDetailBean.recipe.id}"
/>
</h:form> </h:form>
</ui:define> </ui:define>
</ui:composition> </ui:composition>

View File

@ -42,7 +42,7 @@
<p:tab id="overviewTab" title="Shopping List"> <p:tab id="overviewTab" title="Shopping List">
<h:form id="form1"> <h:form id="form1">
<p:dataTable id="tblRecipes" <p:dataTable id="tblRecipes"
style="width: 60em" style="width: 40em"
value="#{shoppingListBean.recipeList}" value="#{shoppingListBean.recipeList}"
var="item" var="item"
> >
@ -87,7 +87,7 @@
> >
<p:dataTable id="tblShopIngredients" <p:dataTable id="tblShopIngredients"
value="#{shoppingListBean.ingredientList}" value="#{shoppingListBean.ingredientList}"
style="width: 60em;" style="width: 40em;"
sortBy="#{item.shopCat}" var="item" sortBy="#{item.shopCat}" var="item"
> >
<f:facet name="header"> <f:facet name="header">

View File

@ -21,16 +21,10 @@ spring:
ddl-auto: none ddl-auto: none
database-platform: org.hibernate.dialect.MySQLDialect database-platform: org.hibernate.dialect.MySQLDialect
# From Maven POM:
appVersion: "@project.version@"
# Tracking-modes prevent URL rewrite jsessionid on Primecases
# resources. Which causes "400" errors on initial main.jsf fetch.
server: server:
servlet: servlet:
session: session:
timeout: '30m' timeout: '30m'
tracking-modes: 'cookie'
# Theme here overrides joinfaces theme # Theme here overrides joinfaces theme
# context-parameters: # context-parameters:
# primefaces: # primefaces:
@ -43,15 +37,4 @@ gourmet:
joinfaces: joinfaces:
primefaces: primefaces:
theme: bluesky theme: casablanca
faces:
project-stage: Production
facelets-libraries: /tags/tags.taglib.xml
#logging:
# level:
# org.springframework.security: TRACE
# org.apache.catalina: TRACE
# jakarta.faces: TRACE
# com.sun.faces: TRACE
# jakarta.servlet: TRACE

View File

@ -1,281 +0,0 @@
-- MariaDB dump 10.19 Distrib 10.5.23-MariaDB, for Linux (x86_64)
--
-- Host: dbase Database: recipes
-- ------------------------------------------------------
-- Server version 10.3.35-MariaDB
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `categories`
--
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`recipe_id` int(11) DEFAULT NULL,
`category` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_CATEGORY_RECIPE` (`recipe_id`),
CONSTRAINT `FK_CATEGORY_RECIPE` FOREIGN KEY (`recipe_id`) REFERENCES `recipe` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=233 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `convtable`
--
DROP TABLE IF EXISTS `convtable`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `convtable` (
`id` int(11) NOT NULL,
`ckey` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
`value` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `crossunitdict`
--
DROP TABLE IF EXISTS `crossunitdict`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `crossunitdict` (
`id` int(11) NOT NULL,
`cukey` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
`value` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `density`
--
DROP TABLE IF EXISTS `density`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `density` (
`id` int(11) NOT NULL,
`dkey` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
`value` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `info`
--
DROP TABLE IF EXISTS `info`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `info` (
`version_super` int(11) DEFAULT NULL,
`version_major` int(11) DEFAULT NULL,
`version_minor` int(11) DEFAULT NULL,
`last_access` int(11) DEFAULT NULL,
`rowid` int(11) NOT NULL,
PRIMARY KEY (`rowid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `ingredients`
--
DROP TABLE IF EXISTS `ingredients`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `ingredients` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`recipe_id` int(11) DEFAULT NULL,
`refid` int(11) DEFAULT NULL,
`unit` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`amount` float DEFAULT NULL,
`rangeamount` float DEFAULT NULL,
`item` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`ingkey` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`optional` tinyint(1) DEFAULT NULL,
`shopoptional` int(11) DEFAULT NULL,
`inggroup` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`position` int(11) DEFAULT NULL,
`deleted` tinyint(1) DEFAULT NULL,
`shopCat_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `refid` (`refid`),
KEY `FK_INGREDIENT_RECIPE` (`recipe_id`),
CONSTRAINT `FK_INGREDIENT_RECIPE` FOREIGN KEY (`recipe_id`) REFERENCES `recipe` (`id`),
CONSTRAINT `CONSTRAINT_1` CHECK (`deleted` in (0,1)),
CONSTRAINT `CONSTRAINT_2` CHECK (`optional` in (0,1))
) ENGINE=InnoDB AUTO_INCREMENT=3540 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `keylookup`
--
DROP TABLE IF EXISTS `keylookup`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `keylookup` (
`id` int(11) NOT NULL,
`word` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`item` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`ingkey` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`count` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `pantry`
--
DROP TABLE IF EXISTS `pantry`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `pantry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ingkey` text COLLATE utf8mb4_bin DEFAULT NULL,
`pantry` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `CONSTRAINT_1` CHECK (`pantry` in (0,1))
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `plugin_info`
--
DROP TABLE IF EXISTS `plugin_info`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `plugin_info` (
`plugin` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`id` int(11) NOT NULL,
`version_super` int(11) DEFAULT NULL,
`version_major` int(11) DEFAULT NULL,
`version_minor` int(11) DEFAULT NULL,
`plugin_version` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `recipe`
--
DROP TABLE IF EXISTS `recipe`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `recipe` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`instructions` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`modifications` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`cuisine` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`rating` int(11) DEFAULT NULL,
`description` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`source` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`preptime` int(11) DEFAULT NULL,
`cooktime` int(11) DEFAULT NULL,
`servings` float DEFAULT NULL,
`yields` float DEFAULT NULL,
`yield_unit` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL,
`image` mediumblob DEFAULT NULL,
`thumb` blob DEFAULT NULL,
`deleted` tinyint(1) DEFAULT NULL,
`recipe_hash` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
`ingredient_hash` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
`link` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`last_modified` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `CONSTRAINT_1` CHECK (`deleted` in (0,1))
) ENGINE=InnoDB AUTO_INCREMENT=535 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `recipe_ingredients`
--
DROP TABLE IF EXISTS `recipe_ingredients`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `recipe_ingredients` (
`Recipe_id` int(11) NOT NULL,
`ingredientHash_id` int(11) NOT NULL,
UNIQUE KEY `ingredientHash_id` (`ingredientHash_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `shopcats`
--
DROP TABLE IF EXISTS `shopcats`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `shopcats` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ingkey` text COLLATE utf8mb4_bin DEFAULT NULL,
`shopcategory` mediumtext COLLATE utf8mb4_bin DEFAULT NULL,
`position` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=679 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `shopcatsorder`
--
DROP TABLE IF EXISTS `shopcatsorder`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `shopcatsorder` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`shopcategory` text COLLATE utf8mb4_bin DEFAULT NULL,
`position` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `unitdict`
--
DROP TABLE IF EXISTS `unitdict`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `unitdict` (
`id` int(11) NOT NULL,
`ukey` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
`value` varchar(150) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2024-02-18 17:34:22

View File

@ -1,17 +0,0 @@
[Unit]
Description=Gourmet Recipe Manager
Documentation=https://gogs.mousetech.com/gourmetj
Requires=network-online.target
After=network-online.target
[Service]
Type=simple
User=gourmetj
WorkingDirectory=/opt/mousetech/gourmetj
ExecStart=/usr/bin/java -jar gourmetj.jar
#Restart=yes
[Install]
WantedBy=multi-user.target
Alias=gourmetj.service

View File

@ -1,25 +0,0 @@
package com.mousetech.gourmetj;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class UserSessionTest {
@Test
void testformatSource() {
UserSession us = new UserSession();
assertEquals("mousetech",
us.urlToSource("www.mousetech.com"));
assertEquals("google",
us.urlToSource("google.com"));
assertEquals("foobar",
us.urlToSource("foobar"));
assertEquals("",
us.urlToSource("\t"));
assertEquals("",
us.urlToSource(null));
}
}