Initial commit.
This commit is contained in:
parent
8385efb9b0
commit
31f9272409
31 changed files with 1795 additions and 0 deletions
42
.classpath
Normal file
42
.classpath
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
|
||||
<attributes>
|
||||
<attribute name="module" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="test" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<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">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.log
|
||||
/target/
|
||||
37
.project
Normal file
37
.project
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>com.stephenschafer.budget.web</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.wst.common.project.facet.core.builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.wst.validation.validationbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
|
||||
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
|
||||
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
12
.settings/.jsdtscope
Normal file
12
.settings/.jsdtscope
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry excluding="**/node_modules/*|**/*.min.js|**/bower_components/*" kind="src" path="src/main/webapp"/>
|
||||
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
|
||||
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.WebProject">
|
||||
<attributes>
|
||||
<attribute name="hide" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.baseBrowserLibrary"/>
|
||||
<classpathentry kind="output" path=""/>
|
||||
</classpath>
|
||||
11
.settings/org.eclipse.jdt.core.prefs
Normal file
11
.settings/org.eclipse.jdt.core.prefs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
eclipse.preferences.version=1
|
||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
|
||||
org.eclipse.jdt.core.compiler.compliance=17
|
||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
||||
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
||||
org.eclipse.jdt.core.compiler.release=enabled
|
||||
org.eclipse.jdt.core.compiler.source=17
|
||||
4
.settings/org.eclipse.m2e.core.prefs
Normal file
4
.settings/org.eclipse.m2e.core.prefs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
activeProfiles=
|
||||
eclipse.preferences.version=1
|
||||
resolveWorkspaceProjects=true
|
||||
version=1
|
||||
13
.settings/org.eclipse.wst.common.component
Normal file
13
.settings/org.eclipse.wst.common.component
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
|
||||
<wb-module deploy-name="budget.web-0.0.1-SNAPSHOT">
|
||||
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
|
||||
<wb-resource deploy-path="/" source-path="/src/main/webapp"/>
|
||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
|
||||
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
|
||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/>
|
||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/java"/>
|
||||
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/resources"/>
|
||||
<property name="java-output-path" value="/com.stephenschafer.budget.web/build/classes"/>
|
||||
<property name="context-root" value="budget.web"/>
|
||||
</wb-module>
|
||||
</project-modules>
|
||||
9
.settings/org.eclipse.wst.common.project.facet.core.xml
Normal file
9
.settings/org.eclipse.wst.common.project.facet.core.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<faceted-project>
|
||||
<fixed facet="jst.web"/>
|
||||
<fixed facet="java"/>
|
||||
<fixed facet="wst.jsdt.web"/>
|
||||
<installed facet="wst.jsdt.web" version="1.0"/>
|
||||
<installed facet="jst.web" version="4.0"/>
|
||||
<installed facet="java" version="17"/>
|
||||
</faceted-project>
|
||||
1
.settings/org.eclipse.wst.jsdt.ui.superType.container
Normal file
1
.settings/org.eclipse.wst.jsdt.ui.superType.container
Normal file
|
|
@ -0,0 +1 @@
|
|||
org.eclipse.wst.jsdt.launching.baseBrowserLibrary
|
||||
1
.settings/org.eclipse.wst.jsdt.ui.superType.name
Normal file
1
.settings/org.eclipse.wst.jsdt.ui.superType.name
Normal file
|
|
@ -0,0 +1 @@
|
|||
Window
|
||||
2
.settings/org.eclipse.wst.validation.prefs
Normal file
2
.settings/org.eclipse.wst.validation.prefs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
disabled=06target
|
||||
eclipse.preferences.version=1
|
||||
23
WebContent/WEB-INF/web.xml
Normal file
23
WebContent/WEB-INF/web.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" version="2.4"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd http://xmlns.jcp.org/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
|
||||
xmlns:web="http://xmlns.jcp.org/xml/ns/javaee">
|
||||
|
||||
<session-config>
|
||||
<session-timeout>1440</session-timeout>
|
||||
</session-config>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>CategoriesPage</servlet-name>
|
||||
<servlet-class>com.stephenschafer.budget.web.CategoriesPage</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>CategoriesPage</servlet-name>
|
||||
<url-pattern>/categories</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
</web-app>
|
||||
8
build-war
Executable file
8
build-war
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
if mvn -f pom.xml clean install > build-jar.log 2> build-jar.err.log; then
|
||||
echo "success"
|
||||
cp target/*.war /tmp
|
||||
else
|
||||
echo "failure"
|
||||
exit 1
|
||||
fi
|
||||
48
pom.xml
Normal file
48
pom.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<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>
|
||||
<groupId>com.stephenschafer</groupId>
|
||||
<artifactId>budget.web</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<packaging>war</packaging>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<configuration>
|
||||
<warSourceDirectory>WebContent</warSourceDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>4.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.38</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>2.18.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.18.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
320
src/main/java/com/stephenschafer/budget/web/CategoriesPage.java
Normal file
320
src/main/java/com/stephenschafer/budget/web/CategoriesPage.java
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
package com.stephenschafer.budget.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.naming.NamingException;
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
public class CategoriesPage extends HttpServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public void init(final ServletConfig config) throws ServletException {
|
||||
try {
|
||||
Configuration.INSTANCE.load();
|
||||
}
|
||||
catch (final Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
super.init(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
var paramYear = request.getParameterValues("year");
|
||||
if (paramYear == null || paramYear.length == 0) {
|
||||
final Calendar cal = new GregorianCalendar();
|
||||
cal.setTime(new Date());
|
||||
final var year = cal.get(Calendar.YEAR);
|
||||
paramYear = new String[] { String.valueOf(year) };
|
||||
}
|
||||
var tmpIncludeExpenses = true;
|
||||
var tmpIncludeIncome = true;
|
||||
var tmpIncludePayments = false;
|
||||
var tmpIncludeInvestments = false;
|
||||
final var includes = request.getParameterValues("include");
|
||||
if (includes != null && includes.length > 0) {
|
||||
tmpIncludeExpenses = false;
|
||||
tmpIncludeIncome = false;
|
||||
tmpIncludePayments = false;
|
||||
tmpIncludeInvestments = false;
|
||||
for (final String include : includes) {
|
||||
if ("expenses".equalsIgnoreCase(include)) {
|
||||
tmpIncludeExpenses = true;
|
||||
}
|
||||
else if ("income".equalsIgnoreCase(include)) {
|
||||
tmpIncludeIncome = true;
|
||||
}
|
||||
else if ("payments".equalsIgnoreCase(include)) {
|
||||
tmpIncludePayments = true;
|
||||
}
|
||||
else if ("investments".equalsIgnoreCase(include)) {
|
||||
tmpIncludeInvestments = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
final var includeExpenses = tmpIncludeExpenses;
|
||||
final var includeIncome = tmpIncludeIncome;
|
||||
final var includePayments = tmpIncludePayments;
|
||||
final var includeInvestments = tmpIncludeInvestments;
|
||||
final var startDates = request.getParameterValues("startDate");
|
||||
final var endDates = request.getParameterValues("endDate");
|
||||
final var startDateString = startDates == null ? null
|
||||
: startDates.length == 0 ? null : startDates[0];
|
||||
final var endDateString = endDates == null ? null
|
||||
: endDates.length == 0 ? null : endDates[0];
|
||||
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
|
||||
final Date startDate;
|
||||
final Date endDate;
|
||||
try {
|
||||
startDate = startDateString == null ? null : df.parse(startDateString);
|
||||
endDate = endDateString == null ? null : df.parse(endDateString);
|
||||
}
|
||||
catch (final ParseException e) {
|
||||
response.sendError(400, "Invalid date");
|
||||
return;
|
||||
}
|
||||
final var out = response.getWriter();
|
||||
response.setHeader("Content-Type", "text/html");
|
||||
try (var connection = Configuration.INSTANCE.getConnection()) {
|
||||
out.println("<html>");
|
||||
out.println("<head>");
|
||||
final var js = Util.getResourceAsString("categories.js");
|
||||
out.println("<script>");
|
||||
out.println(js);
|
||||
out.println("</script>");
|
||||
final var css = Util.getResourceAsString("categories.css");
|
||||
out.println("<style>");
|
||||
out.println(css);
|
||||
out.println("</style>");
|
||||
out.println("</head>");
|
||||
out.println("<body>");
|
||||
for (final String year : paramYear) {
|
||||
final var categoryMap = loadCategories(connection, year, includeIncome,
|
||||
includePayments, includeInvestments, includeExpenses);
|
||||
final var sql = Util.getResourceAsString("getTransactionDetail.sql") //
|
||||
.replace("${databaseName}", "budget_" + year) //
|
||||
.replace("${regexDatabaseName}", "budget");
|
||||
try (var stmt = connection.prepareStatement(sql)) {
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
final var detail = new Detail();
|
||||
var i = 0;
|
||||
detail.transactionId = rs.getInt(++i);
|
||||
detail.date = rs.getDate(++i);
|
||||
detail.source = rs.getString(++i);
|
||||
detail.description = rs.getString(++i);
|
||||
detail.amount = rs.getBigDecimal(++i);
|
||||
final var categoryId = rs.getInt(++i);
|
||||
detail.regex = rs.getString(++i);
|
||||
detail.flags = rs.getInt(++i);
|
||||
detail.requiredSource = rs.getString(++i);
|
||||
detail.extraDescription = rs.getString(++i);
|
||||
final var category = categoryMap.get(categoryId);
|
||||
if (category != null && //
|
||||
detail.isAfter(startDate) && //
|
||||
detail.isBefore(endDate) && //
|
||||
category.isIncluded()) {
|
||||
category.addDetail(detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final var rootCategory = new Category(-1, null, "root");
|
||||
for (final Integer categoryId : categoryMap.keySet()) {
|
||||
final var category = categoryMap.get(categoryId);
|
||||
if ((category != null) && (category.getParent() == null)) {
|
||||
rootCategory.addChild(category);
|
||||
}
|
||||
}
|
||||
out.println("<div class=\"tableFixHead\">");
|
||||
out.println("<h1>" + year + " Categories</h1>");
|
||||
out.println("<table>");
|
||||
out.println("<thead>");
|
||||
final var grandHead = Util.getResourceAsString("grandHead.html");
|
||||
out.println(grandHead);
|
||||
out.println("</thead>");
|
||||
out.println("<tbody>");
|
||||
generateCategoryTable(out, rootCategory, 0);
|
||||
out.println("</tbody>");
|
||||
out.println("</table>");
|
||||
out.println("</div>");
|
||||
}
|
||||
out.println("</body>");
|
||||
out.println("</html>");
|
||||
}
|
||||
catch (final SQLException | NamingException e) {
|
||||
throw new ServletException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateCategoryTable(final PrintWriter out, final Category parent,
|
||||
final int level) throws IOException {
|
||||
final var details = parent.getDetails();
|
||||
final var categoryAmount = parent.getDetailTotal();
|
||||
final var categoryGrandAmount = parent.getGrandTotal();
|
||||
var categoryHeadHtml = Util.getResourceAsString("categoryHead.html") //
|
||||
.replace("${name}", parent.getName()) //
|
||||
.replace("${id}", parent.getId().toString()) //
|
||||
.replace("${amount}", categoryAmount.toString()) //
|
||||
.replace("${total}", categoryGrandAmount.toString()) //
|
||||
.replace("${indent}", String.valueOf(level * 20) + "px");
|
||||
final int largestMonth = parent.getLargestMonth();
|
||||
for (int month = 0; month < 12; month++) {
|
||||
categoryHeadHtml = categoryHeadHtml //
|
||||
.replace("${total-" + month + "}", parent.getMonthGrandTotal(month).toString()) //
|
||||
.replace("${color-" + month + "}", month == largestMonth ? "color: red; " : "");
|
||||
}
|
||||
out.print(categoryHeadHtml);
|
||||
final var categoryDetailIntroHtml = Util.getResourceAsString("categoryDetailIntro.html") //
|
||||
.replace("${id}", parent.getId().toString()) //
|
||||
.replace("${indent}", String.valueOf(level * 10) + "px");
|
||||
out.print(categoryDetailIntroHtml);
|
||||
if (!details.isEmpty()) {
|
||||
final var categoryDetailHeadHtml = Util.getResourceAsString("categoryDetailHead.html");
|
||||
out.print(categoryDetailHeadHtml);
|
||||
}
|
||||
Collections.sort(details);
|
||||
for (final Detail detail : details) {
|
||||
final var categoryDetailHtml = Util.getResourceAsString("categoryDetail.html") //
|
||||
.replace("${source}", detail.source) //
|
||||
.replace("${description}", detail.description) //
|
||||
.replace("${date}", detail.date.toString()) //
|
||||
.replace("${amount}", detail.amount.toString()) //
|
||||
.replace("${extraDescription}",
|
||||
detail.extraDescription == null ? "" : detail.extraDescription) //
|
||||
.replace("${regex}", detail.regex) //
|
||||
.replace("${flags}", getFlagsString(detail.flags)) //
|
||||
.replace("${requiredSource}",
|
||||
detail.requiredSource == null ? "" : detail.requiredSource)//
|
||||
;
|
||||
out.print(categoryDetailHtml);
|
||||
}
|
||||
out.println("</table>");
|
||||
out.println("</td>");
|
||||
out.println("</tr>");
|
||||
final var categories = parent.getChildCategories();
|
||||
for (final Category category : categories) {
|
||||
generateCategoryTable(out, category, level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private String getFlagsString(final int flags) {
|
||||
final List<String> flagStrings = new ArrayList<>();
|
||||
if ((flags & Pattern.CASE_INSENSITIVE) != 0) {
|
||||
flagStrings.add("Case Insensitive");
|
||||
}
|
||||
if ((flags & Pattern.MULTILINE) != 0) {
|
||||
flagStrings.add("Multiline");
|
||||
}
|
||||
if ((flags & Pattern.DOTALL) != 0) {
|
||||
flagStrings.add("Dotall");
|
||||
}
|
||||
if ((flags & Pattern.UNICODE_CASE) != 0) {
|
||||
flagStrings.add("Unicode Case");
|
||||
}
|
||||
if ((flags & Pattern.CANON_EQ) != 0) {
|
||||
flagStrings.add("Canon EQ");
|
||||
}
|
||||
if ((flags & Pattern.UNIX_LINES) != 0) {
|
||||
flagStrings.add("Unix Linex");
|
||||
}
|
||||
if ((flags & Pattern.LITERAL) != 0) {
|
||||
flagStrings.add("Literal");
|
||||
}
|
||||
if ((flags & Pattern.UNICODE_CHARACTER_CLASS) != 0) {
|
||||
flagStrings.add("Unicode Character Class");
|
||||
}
|
||||
if ((flags & Pattern.COMMENTS) != 0) {
|
||||
flagStrings.add("Comments");
|
||||
}
|
||||
return flagStrings.toString();
|
||||
}
|
||||
|
||||
interface CategoryFilter {
|
||||
boolean include(Category category);
|
||||
}
|
||||
|
||||
private Map<Integer, Category> loadCategories(final Connection connection, final String year,
|
||||
final boolean includeIncome, final boolean includePayments,
|
||||
final boolean includeInvestments, final boolean includeExpenses)
|
||||
throws SQLException, IOException {
|
||||
Category unknownCategory = null;
|
||||
final Map<Integer, Category> categories = new HashMap<>();
|
||||
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
|
||||
"budget");
|
||||
try (var stmt = connection.prepareStatement(sql)) {
|
||||
try (var rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
final Integer id = rs.getInt(1);
|
||||
Integer parentId = rs.getInt(2);
|
||||
if (rs.wasNull()) {
|
||||
parentId = null;
|
||||
}
|
||||
final var name = rs.getString(3);
|
||||
final var category = new Category(id, parentId, name);
|
||||
categories.put(id, category);
|
||||
if ("unknown".equals(name) && parentId == null) {
|
||||
unknownCategory = category;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final Integer categoryId : categories.keySet()) {
|
||||
final var category = categories.get(categoryId);
|
||||
category.updateParent(categories);
|
||||
}
|
||||
if (unknownCategory == null) {
|
||||
unknownCategory = new Category(-1, null, "unknown");
|
||||
categories.put(unknownCategory.getId(), unknownCategory);
|
||||
}
|
||||
for (final Integer categoryId : categories.keySet()) {
|
||||
final var category = categories.get(categoryId);
|
||||
final CategoryFilter filter = category1 -> {
|
||||
final String categoryName = category1.getName();
|
||||
if ("income".equals(categoryName) || "refunds".equals(categoryName)) {
|
||||
if (!includeIncome) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if ("payment".equals(categoryName)) {
|
||||
if (!includePayments) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if ("investment".equals(categoryName)) {
|
||||
if (!includeInvestments) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!includeExpenses) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
final boolean included = filter.include(category);
|
||||
category.setIncluded(included);
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
169
src/main/java/com/stephenschafer/budget/web/Category.java
Normal file
169
src/main/java/com/stephenschafer/budget/web/Category.java
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
package com.stephenschafer.budget.web;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Category {
|
||||
private final Integer id;
|
||||
private final Integer parentId;
|
||||
private final String name;
|
||||
private Category parent;
|
||||
private boolean included;
|
||||
private List<Detail> details = new ArrayList<>();
|
||||
private final Map<Integer, Category> children = new HashMap<>();
|
||||
|
||||
public Category(final Integer id, final Integer parentId, final String name) {
|
||||
this.id = id;
|
||||
this.parentId = parentId;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void updateParent(final Map<Integer, Category> categories) {
|
||||
if (parentId == null) {
|
||||
parent = null;
|
||||
}
|
||||
else {
|
||||
parent = categories.get(parentId);
|
||||
parent.addChild(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void addChild(final Category category) {
|
||||
children.put(category.id, category);
|
||||
}
|
||||
|
||||
public Category getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public void setParent(final Category parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Integer getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getQualifiedName() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
if (parent != null) {
|
||||
sb.append(parent.getQualifiedName());
|
||||
sb.append(".");
|
||||
}
|
||||
sb.append(name);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public List<Detail> getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(final List<Detail> details) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public void addDetail(final Detail detail) {
|
||||
details.add(detail);
|
||||
}
|
||||
|
||||
public Map<Integer, Category> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public BigDecimal getDetailTotal() {
|
||||
var amount = new BigDecimal(0);
|
||||
for (final Detail detail : details) {
|
||||
amount = amount.add(detail.amount);
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
public BigDecimal getMonthTotal(final int month) {
|
||||
var amount = new BigDecimal(0);
|
||||
for (final Detail detail : details) {
|
||||
final Calendar cal = new GregorianCalendar();
|
||||
cal.setTime(detail.date);
|
||||
if (month == cal.get(Calendar.MONTH)) {
|
||||
amount = amount.add(detail.amount);
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
public BigDecimal getGrandTotal() {
|
||||
var total = getDetailTotal();
|
||||
for (final Integer categoryId : children.keySet()) {
|
||||
final var category = children.get(categoryId);
|
||||
total = total.add(category.getGrandTotal());
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public BigDecimal getMonthGrandTotal(final int month) {
|
||||
var total = getMonthTotal(month);
|
||||
for (final Integer categoryId : children.keySet()) {
|
||||
final var category = children.get(categoryId);
|
||||
total = total.add(category.getMonthGrandTotal(month));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public int getLargestMonth() {
|
||||
BigDecimal max = new BigDecimal(0);
|
||||
int maxMonth = -1;
|
||||
int maxCount = 0;
|
||||
for (int month = 0; month < 12; month++) {
|
||||
final BigDecimal monthAmount = getMonthGrandTotal(month);
|
||||
if (monthAmount.compareTo(max) == 0) {
|
||||
maxCount++;
|
||||
}
|
||||
else if (monthAmount.compareTo(max) > 0) {
|
||||
max = monthAmount;
|
||||
maxMonth = month;
|
||||
maxCount = 1;
|
||||
}
|
||||
}
|
||||
return maxCount > 1 ? -1 : maxMonth;
|
||||
}
|
||||
|
||||
public List<Category> getChildCategories() {
|
||||
final List<Category> categories = new ArrayList<>();
|
||||
for (final Integer categoryId : children.keySet()) {
|
||||
final var category = children.get(categoryId);
|
||||
categories.add(category);
|
||||
}
|
||||
categories.sort((arg0, arg1) -> {
|
||||
final var name0 = arg0.getName();
|
||||
final var name1 = arg1.getName();
|
||||
final var comparison = name0.compareTo(name1);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
final var id0 = arg0.getId();
|
||||
final var id1 = arg1.getId();
|
||||
return id0.compareTo(id1);
|
||||
});
|
||||
return categories;
|
||||
}
|
||||
|
||||
public boolean isIncluded() {
|
||||
return included;
|
||||
}
|
||||
|
||||
public void setIncluded(final boolean include) {
|
||||
this.included = include;
|
||||
}
|
||||
}
|
||||
120
src/main/java/com/stephenschafer/budget/web/Configuration.java
Normal file
120
src/main/java/com/stephenschafer/budget/web/Configuration.java
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package com.stephenschafer.budget.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.naming.InitialContext;
|
||||
import javax.naming.NamingException;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public class Configuration {
|
||||
public static final Configuration INSTANCE;
|
||||
private String jndiName;
|
||||
private DbConnectionPool pool;
|
||||
private String privilegedHost;
|
||||
private String defaultDomain;
|
||||
private boolean loaded;
|
||||
|
||||
private Configuration() {
|
||||
jndiName = null;
|
||||
pool = null;
|
||||
loaded = false;
|
||||
}
|
||||
|
||||
public void load() throws IOException, ClassNotFoundException, SQLException {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
final var systemProps = System.getProperties();
|
||||
final var catalinaHome = systemProps.getProperty("catalina.home");
|
||||
var propertiesFileName = System.getenv("BUDGET_PROPERTIES");
|
||||
if (propertiesFileName == null) {
|
||||
propertiesFileName = catalinaHome + "/conf/Catalina/localhost/budget.properties";
|
||||
}
|
||||
final var propertiesFile = new File(propertiesFileName);
|
||||
final var properties = new Properties();
|
||||
try (var fis = new FileInputStream(propertiesFile)) {
|
||||
properties.load(fis);
|
||||
}
|
||||
Logger.logFilename = properties.getProperty("log.filename");
|
||||
Logger.log("********************************* Starting");
|
||||
jndiName = properties.getProperty("db.jndi");
|
||||
final var url = properties.getProperty("db.url");
|
||||
final var username = properties.getProperty("db.username");
|
||||
final var password = properties.getProperty("db.password");
|
||||
if (jndiName == null) {
|
||||
pool = new DbConnectionPool("com.mysql.jdbc.Driver", url, username, password);
|
||||
}
|
||||
privilegedHost = properties.getProperty("priv-host");
|
||||
final var defaultDomain = properties.getProperty("default-domain");
|
||||
this.defaultDomain = defaultDomain == null ? "schafer.cc" : defaultDomain;
|
||||
Logger.log("initialized, jndiName = " + jndiName);
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
public Connection getConnection() throws SQLException, NamingException {
|
||||
final var jndiName = getJndiName();
|
||||
if (jndiName != null) {
|
||||
final var initialContext = new InitialContext();
|
||||
final var datasource = (DataSource) initialContext.lookup(jndiName);
|
||||
SQLException lastException = null;
|
||||
var i = 0;
|
||||
while (i < 4) {
|
||||
try {
|
||||
return datasource.getConnection();
|
||||
}
|
||||
catch (final SQLException e) {
|
||||
Logger.log("Failed to get connection, " + (3 - i) + " retrys left", e);
|
||||
lastException = e;
|
||||
try {
|
||||
Thread.sleep(5000L);
|
||||
}
|
||||
catch (final InterruptedException ex) {
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
return getPool().getConnection(8, false, true);
|
||||
}
|
||||
|
||||
public String getJndiName() {
|
||||
return jndiName;
|
||||
}
|
||||
|
||||
public void setJndiName(final String jndiName) {
|
||||
this.jndiName = jndiName;
|
||||
}
|
||||
|
||||
public DbConnectionPool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public void setPool(final DbConnectionPool pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
static {
|
||||
INSTANCE = new Configuration();
|
||||
}
|
||||
|
||||
public String getPrivilegedHost() {
|
||||
return privilegedHost;
|
||||
}
|
||||
|
||||
public String getDefaultDomain() {
|
||||
return defaultDomain;
|
||||
}
|
||||
|
||||
public void setDefaultDomain(final String defaultDomain) {
|
||||
this.defaultDomain = defaultDomain;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,690 @@
|
|||
package com.stephenschafer.budget.web;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.SocketException;
|
||||
import java.sql.Array;
|
||||
import java.sql.Blob;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.Clob;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.NClob;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLClientInfoException;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLWarning;
|
||||
import java.sql.SQLXML;
|
||||
import java.sql.Savepoint;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Struct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class DbConnectionPool implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger LOGGER;
|
||||
private final String uRL;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String driver;
|
||||
private final Queue<PooledConnection> connections;
|
||||
private long openConnectionIndex;
|
||||
private final Map<Long, OpenConnectionInfo> openConnections;
|
||||
private long timeout;
|
||||
static {
|
||||
LOGGER = Logger.getLogger(DbConnectionPool.class.getName());
|
||||
}
|
||||
|
||||
public DbConnectionPool(final String dbDriver, final String dbName, final String dbUsername,
|
||||
final String dbPassword) throws ClassNotFoundException, SQLException {
|
||||
connections = new LinkedList<>();
|
||||
openConnectionIndex = 0L;
|
||||
openConnections = new HashMap<>();
|
||||
timeout = 900000L;
|
||||
driver = dbDriver;
|
||||
uRL = dbName;
|
||||
username = dbUsername;
|
||||
password = dbPassword;
|
||||
validateConnection();
|
||||
}
|
||||
|
||||
public final void validateConnection() throws ClassNotFoundException, SQLException {
|
||||
DbConnectionPool.LOGGER.log(Level.FINEST, "Testing connection pool");
|
||||
DbConnectionPool.LOGGER.log(Level.FINEST, "Instantiating " + driver + "\n");
|
||||
Class.forName(driver);
|
||||
DbConnectionPool.LOGGER.log(Level.FINEST, "Connecting");
|
||||
final Connection connection = DriverManager.getConnection(uRL, username, password);
|
||||
try {
|
||||
final DatabaseMetaData dbmd = connection.getMetaData();
|
||||
DbConnectionPool.LOGGER.log(Level.FINEST,
|
||||
"Connection to " + dbmd.getDatabaseProductName() + " "
|
||||
+ dbmd.getDatabaseProductVersion() + " successful.");
|
||||
}
|
||||
finally {
|
||||
connection.close();
|
||||
}
|
||||
connection.close();
|
||||
}
|
||||
|
||||
public final void clear() throws SQLException {
|
||||
synchronized (this) {
|
||||
while (!connections.isEmpty()) {
|
||||
final PooledConnection connection = connections.remove();
|
||||
connection.reallyClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final PooledConnection getConnection(final int transactionIsolation,
|
||||
final boolean readOnly, final boolean autoCommit) throws SQLException {
|
||||
while (true) {
|
||||
PooledConnection oldConnection;
|
||||
final long timeout;
|
||||
synchronized (this) {
|
||||
if (connections.isEmpty()) {
|
||||
oldConnection = null;
|
||||
}
|
||||
else {
|
||||
oldConnection = connections.remove();
|
||||
}
|
||||
timeout = this.timeout;
|
||||
}
|
||||
if (oldConnection == null) {
|
||||
DbConnectionPool.LOGGER.log(Level.FINEST, "Connecting");
|
||||
SQLException exception = null;
|
||||
int retryCount = 0;
|
||||
while (retryCount < 10) {
|
||||
Connection newConnection;
|
||||
try {
|
||||
newConnection = DriverManager.getConnection(uRL, username, password);
|
||||
}
|
||||
catch (final SQLException e) {
|
||||
if (!(e.getCause() instanceof SocketException)) {
|
||||
throw e;
|
||||
}
|
||||
exception = e;
|
||||
DbConnectionPool.LOGGER.log(Level.FINEST, "Retrying", e);
|
||||
try {
|
||||
Thread.sleep(1000L);
|
||||
}
|
||||
catch (final InterruptedException ex) {
|
||||
}
|
||||
++retryCount;
|
||||
newConnection = null;
|
||||
}
|
||||
if (newConnection != null) {
|
||||
newConnection.setAutoCommit(true);
|
||||
newConnection.setTransactionIsolation(transactionIsolation);
|
||||
newConnection.setReadOnly(readOnly);
|
||||
newConnection.setAutoCommit(autoCommit);
|
||||
final long index = incrementOpenConnectionCount();
|
||||
return new PooledConnection(newConnection, index);
|
||||
}
|
||||
}
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
if (oldConnection != null) {
|
||||
if (oldConnection.isClosed()) {
|
||||
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||
"Pooled connection was already closed");
|
||||
}
|
||||
else {
|
||||
Label_0430: {
|
||||
Label_0418: {
|
||||
if ((timeout != 0L) && (System.currentTimeMillis()
|
||||
- oldConnection.getLastAccess() >= timeout)) {
|
||||
break Label_0418;
|
||||
}
|
||||
try {
|
||||
oldConnection.setAutoCommit(true);
|
||||
oldConnection.setTransactionIsolation(transactionIsolation);
|
||||
oldConnection.setReadOnly(readOnly);
|
||||
oldConnection.setAutoCommit(autoCommit);
|
||||
synchronized (this) {
|
||||
final Long key = Long.valueOf(oldConnection.getIndex());
|
||||
final OpenConnectionInfo info = openConnections.get(key);
|
||||
if (info != null) {
|
||||
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||
"Overwriting open connection info: " + key + " "
|
||||
+ info);
|
||||
}
|
||||
openConnections.put(key, new OpenConnectionInfo());
|
||||
}
|
||||
return oldConnection;
|
||||
}
|
||||
catch (final Exception e2) {
|
||||
DbConnectionPool.LOGGER.log(Level.SEVERE,
|
||||
"Unable to reuse DB connection", e2);
|
||||
break Label_0430;
|
||||
}
|
||||
}
|
||||
DbConnectionPool.LOGGER.log(Level.FINEST, "DB connection timed out");
|
||||
try {
|
||||
oldConnection.reallyClose();
|
||||
}
|
||||
catch (final Exception e2) {
|
||||
DbConnectionPool.LOGGER.log(Level.SEVERE,
|
||||
"Unable to really close DB connection", e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long incrementOpenConnectionCount() {
|
||||
synchronized (this) {
|
||||
final long index = openConnectionIndex++;
|
||||
final Long key = Long.valueOf(index);
|
||||
final OpenConnectionInfo info = openConnections.get(key);
|
||||
if (info != null) {
|
||||
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||
"Overwriting open connection info: " + key + " " + info);
|
||||
}
|
||||
openConnections.put(key, new OpenConnectionInfo());
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
protected final void add(final PooledConnection connection) {
|
||||
synchronized (this) {
|
||||
final OpenConnectionInfo info = openConnections.remove(
|
||||
Long.valueOf(connection.getIndex()));
|
||||
if (info == null) {
|
||||
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||
"adding orphaned connection: " + connection.getIndex());
|
||||
}
|
||||
else {
|
||||
connections.offer(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final long getOpenConnectionIndex() {
|
||||
synchronized (this) {
|
||||
return openConnectionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public final OpenConnectionInfo getOpenConnectionInfo(final long index) {
|
||||
synchronized (this) {
|
||||
return openConnections.get(Long.valueOf(index));
|
||||
}
|
||||
}
|
||||
|
||||
public final int getOpenConnectionCount() {
|
||||
synchronized (this) {
|
||||
return openConnections.size();
|
||||
}
|
||||
}
|
||||
|
||||
public final int getPooledConnectionCount() {
|
||||
synchronized (this) {
|
||||
return connections.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String toString() {
|
||||
final StringBuilder buf = new StringBuilder();
|
||||
buf.append("DB ");
|
||||
buf.append(uRL);
|
||||
buf.append(" ");
|
||||
synchronized (this) {
|
||||
buf.append("open: ");
|
||||
buf.append(openConnections.size());
|
||||
buf.append(", pooled: ");
|
||||
buf.append(connections.size());
|
||||
buf.append(", next: ");
|
||||
buf.append(openConnectionIndex);
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
public final long getTimeout() {
|
||||
synchronized (this) {
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
|
||||
public final void setTimeout(final long timeout) {
|
||||
synchronized (this) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
public final String getDriver() {
|
||||
return driver;
|
||||
}
|
||||
|
||||
public final String getURL() {
|
||||
return uRL;
|
||||
}
|
||||
|
||||
public final String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public final String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public synchronized Map<Long, Long> getOpenConnections() {
|
||||
final Map<Long, Long> map = new HashMap<>();
|
||||
for (final Long index : openConnections.keySet()) {
|
||||
final OpenConnectionInfo info = openConnections.get(index);
|
||||
map.put(index, Long.valueOf(info.timestamp));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public static final class OpenConnectionInfo {
|
||||
public final long timestamp;
|
||||
private final List<StackTraceElement> stackTrace;
|
||||
|
||||
public OpenConnectionInfo() {
|
||||
timestamp = System.currentTimeMillis();
|
||||
final StackTraceElement[] steArray = Thread.currentThread().getStackTrace();
|
||||
final List<StackTraceElement> stackTrace = new ArrayList<>(
|
||||
steArray.length);
|
||||
StackTraceElement[] array;
|
||||
for (int length = (array = steArray).length, i = 0; i < length; ++i) {
|
||||
final StackTraceElement element = array[i];
|
||||
stackTrace.add(element);
|
||||
}
|
||||
this.stackTrace = Collections.unmodifiableList(stackTrace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("Timestamp: ");
|
||||
sb.append(new Date(timestamp));
|
||||
sb.append("\n");
|
||||
for (final StackTraceElement element : stackTrace) {
|
||||
sb.append(element);
|
||||
sb.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public class PooledConnection implements Connection {
|
||||
private final Connection connection;
|
||||
private long lastAccess;
|
||||
private boolean autoCommit;
|
||||
private boolean readOnly;
|
||||
private final long index;
|
||||
|
||||
public PooledConnection(final Connection connection, final long index) {
|
||||
lastAccess = System.currentTimeMillis();
|
||||
autoCommit = false;
|
||||
readOnly = false;
|
||||
this.index = index;
|
||||
this.connection = connection;
|
||||
try {
|
||||
autoCommit = connection.getAutoCommit();
|
||||
}
|
||||
catch (final SQLException e) {
|
||||
DbConnectionPool.LOGGER.log(Level.WARNING, "Unable to get auto commit", e);
|
||||
}
|
||||
try {
|
||||
readOnly = connection.isReadOnly();
|
||||
}
|
||||
catch (final SQLException e) {
|
||||
DbConnectionPool.LOGGER.log(Level.WARNING, "Unable to get read only", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearPool() throws SQLException {
|
||||
clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nativeSQL(final String sql) throws SQLException {
|
||||
return connection.nativeSQL(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return connection.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Class<?>> getTypeMap() throws SQLException {
|
||||
return connection.getTypeMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreparedStatement prepareStatement(final String sql) throws SQLException {
|
||||
final PreparedStatement stmt = connection.prepareStatement(sql);
|
||||
if (connection.getAutoCommit() != autoCommit || connection.isReadOnly() != readOnly) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
String sep = "";
|
||||
if (connection.getAutoCommit() != autoCommit) {
|
||||
sb.append(sep);
|
||||
sep = " and ";
|
||||
sb.append("autoCommit has changed from " + autoCommit);
|
||||
}
|
||||
if (connection.isReadOnly() != readOnly) {
|
||||
sb.append(sep);
|
||||
sep = " and ";
|
||||
sb.append("readOnly has changed from " + readOnly);
|
||||
}
|
||||
sb.append(" in ");
|
||||
sb.append(stmt);
|
||||
DbConnectionPool.LOGGER.log(Level.WARNING, sb.toString());
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransactionIsolation(final int level) throws SQLException {
|
||||
connection.setTransactionIsolation(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCatalog() throws SQLException {
|
||||
return connection.getCatalog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTransactionIsolation() throws SQLException {
|
||||
return connection.getTransactionIsolation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseSavepoint(final Savepoint savepoint) throws SQLException {
|
||||
connection.releaseSavepoint(savepoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHoldability() throws SQLException {
|
||||
return connection.getHoldability();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallableStatement prepareCall(final String sql, final int resultSetType,
|
||||
final int resultSetConcurrency, final int resultSetHoldability)
|
||||
throws SQLException {
|
||||
return connection.prepareCall(sql, resultSetType, resultSetConcurrency,
|
||||
resultSetHoldability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAutoCommit() throws SQLException {
|
||||
return connection.getAutoCommit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement createStatement() throws SQLException {
|
||||
return connection.createStatement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallableStatement prepareCall(final String sql) throws SQLException {
|
||||
return connection.prepareCall(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAutoCommit(final boolean autoCommit) throws SQLException {
|
||||
this.autoCommit = autoCommit;
|
||||
connection.setAutoCommit(autoCommit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys)
|
||||
throws SQLException {
|
||||
return connection.prepareStatement(sql, autoGeneratedKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadOnly(final boolean readOnly) throws SQLException {
|
||||
this.readOnly = readOnly;
|
||||
connection.setReadOnly(readOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallableStatement prepareCall(final String sql, final int resultSetType,
|
||||
final int resultSetConcurrency) throws SQLException {
|
||||
return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SQLWarning getWarnings() throws SQLException {
|
||||
return connection.getWarnings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreparedStatement prepareStatement(final String sql, final int resultSetType,
|
||||
final int resultSetConcurrency) throws SQLException {
|
||||
return connection.prepareStatement(sql, resultSetType, resultSetConcurrency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
return connection.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes)
|
||||
throws SQLException {
|
||||
return connection.prepareStatement(sql, columnIndexes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() throws SQLException {
|
||||
return connection.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreparedStatement prepareStatement(final String sql, final int resultSetType,
|
||||
final int resultSetConcurrency, final int resultSetHoldability)
|
||||
throws SQLException {
|
||||
return connection.prepareStatement(sql, resultSetType, resultSetConcurrency,
|
||||
resultSetHoldability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() throws SQLException {
|
||||
connection.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearWarnings() throws SQLException {
|
||||
connection.clearWarnings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCatalog(final String catalog) throws SQLException {
|
||||
connection.setCatalog(catalog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
add(this);
|
||||
lastAccess = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void reallyClose() throws SQLException {
|
||||
connection.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return connection.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseMetaData getMetaData() throws SQLException {
|
||||
return connection.getMetaData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() throws SQLException {
|
||||
connection.rollback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Savepoint setSavepoint(final String name) throws SQLException {
|
||||
return connection.setSavepoint(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() throws SQLException {
|
||||
return connection.isReadOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement createStatement(final int resultSetType, final int resultSetConcurrency)
|
||||
throws SQLException {
|
||||
return connection.createStatement(resultSetType, resultSetConcurrency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback(final Savepoint savepoint) throws SQLException {
|
||||
connection.rollback(savepoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreparedStatement prepareStatement(final String sql, final String[] columnNames)
|
||||
throws SQLException {
|
||||
return connection.prepareStatement(sql, columnNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Savepoint setSavepoint() throws SQLException {
|
||||
return connection.setSavepoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement createStatement(final int resultSetType, final int resultSetConcurrency,
|
||||
final int resultSetHoldability) throws SQLException {
|
||||
return connection.createStatement(resultSetType, resultSetConcurrency,
|
||||
resultSetHoldability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTypeMap(final Map<String, Class<?>> map) throws SQLException {
|
||||
connection.setTypeMap(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHoldability(final int holdability) throws SQLException {
|
||||
connection.setHoldability(holdability);
|
||||
}
|
||||
|
||||
public long getLastAccess() {
|
||||
return lastAccess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Array createArrayOf(final String arg0, final Object[] arg1) throws SQLException {
|
||||
return connection.createArrayOf(arg0, arg1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Blob createBlob() throws SQLException {
|
||||
return connection.createBlob();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Clob createClob() throws SQLException {
|
||||
return connection.createClob();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NClob createNClob() throws SQLException {
|
||||
return connection.createNClob();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SQLXML createSQLXML() throws SQLException {
|
||||
return connection.createSQLXML();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Struct createStruct(final String arg0, final Object[] arg1) throws SQLException {
|
||||
return connection.createStruct(arg0, arg1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Properties getClientInfo() throws SQLException {
|
||||
return connection.getClientInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientInfo(final String arg0) throws SQLException {
|
||||
return connection.getClientInfo(arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(final int arg0) throws SQLException {
|
||||
return connection.isValid(arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientInfo(final Properties arg0) throws SQLClientInfoException {
|
||||
connection.setClientInfo(arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientInfo(final String arg0, final String arg1)
|
||||
throws SQLClientInfoException {
|
||||
connection.setClientInfo(arg0, arg1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWrapperFor(final Class<?> arg0) throws SQLException {
|
||||
return connection.isWrapperFor(arg0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unwrap(final Class<T> arg0) throws SQLException {
|
||||
return connection.unwrap(arg0);
|
||||
}
|
||||
|
||||
public long getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSchema(final String schema) throws SQLException {
|
||||
connection.setSchema(schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSchema() throws SQLException {
|
||||
return connection.getSchema();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void abort(final Executor executor) throws SQLException {
|
||||
connection.abort(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNetworkTimeout(final Executor executor, final int milliseconds)
|
||||
throws SQLException {
|
||||
connection.setNetworkTimeout(executor, milliseconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNetworkTimeout() throws SQLException {
|
||||
return connection.getNetworkTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/main/java/com/stephenschafer/budget/web/Detail.java
Normal file
73
src/main/java/com/stephenschafer/budget/web/Detail.java
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package com.stephenschafer.budget.web;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
class Detail implements Comparable<Detail> {
|
||||
int transactionId;
|
||||
String source;
|
||||
String description;
|
||||
Date date;
|
||||
BigDecimal amount;
|
||||
String regex;
|
||||
int flags;
|
||||
String requiredSource;
|
||||
String extraDescription;
|
||||
|
||||
@Override
|
||||
public int compareTo(final Detail arg1) {
|
||||
final var arg0 = this;
|
||||
int comparison;
|
||||
if (arg0.date == null) {
|
||||
if (arg1.date != null) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (arg1.date == null) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
comparison = arg0.date.compareTo(arg1.date);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
if (arg0.source == null) {
|
||||
if (arg1.source != null) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (arg1.source == null) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
comparison = arg0.source.compareTo(arg1.source);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
if (arg0.description == null) {
|
||||
if (arg1.description != null) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (arg1.description == null) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
comparison = arg0.description.compareTo(arg1.description);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean isAfter(final Date startDate) {
|
||||
return startDate == null || startDate.getTime() <= this.date.getTime();
|
||||
}
|
||||
|
||||
public boolean isBefore(final Date endDate) {
|
||||
return endDate == null || endDate.getTime() > this.date.getTime();
|
||||
}
|
||||
}
|
||||
76
src/main/java/com/stephenschafer/budget/web/Logger.java
Normal file
76
src/main/java/com/stephenschafer/budget/web/Logger.java
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package com.stephenschafer.budget.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class Logger {
|
||||
public static String logFilename;
|
||||
public static boolean debugLogging;
|
||||
|
||||
public static void debugLog(final Object message) {
|
||||
if (Logger.debugLogging) {
|
||||
log(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void debugLog(final Object message, final Throwable t) {
|
||||
if (Logger.debugLogging) {
|
||||
log(message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void log(final Object message) {
|
||||
log(message, null);
|
||||
}
|
||||
|
||||
public static void log(final Object message, final Throwable t) {
|
||||
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ");
|
||||
try {
|
||||
final String filename = Logger.logFilename;
|
||||
if (filename == null) {
|
||||
final PrintStream out = System.out;
|
||||
out.print(df.format(new Date()));
|
||||
out.print(" ");
|
||||
out.println(message);
|
||||
if (t != null) {
|
||||
t.printStackTrace(out);
|
||||
}
|
||||
}
|
||||
else {
|
||||
final File file = new File(filename);
|
||||
final FileWriter fw = new FileWriter(file, true);
|
||||
final PrintWriter pw = new PrintWriter(fw);
|
||||
try {
|
||||
pw.print(df.format(new Date()));
|
||||
pw.print(" ");
|
||||
pw.println(message);
|
||||
if (t != null) {
|
||||
t.printStackTrace(pw);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
pw.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (final IOException e) {
|
||||
System.out.println("Logger: Unable to log " + message);
|
||||
if (t != null) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
log("Log failure caused by:");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
Logger.logFilename = null;
|
||||
Logger.debugLogging = false;
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/stephenschafer/budget/web/Util.java
Normal file
22
src/main/java/com/stephenschafer/budget/web/Util.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package com.stephenschafer.budget.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
|
||||
public class Util {
|
||||
public static String getResourceAsString(final String resourceName) throws IOException {
|
||||
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
try (final Reader reader = new InputStreamReader(
|
||||
classLoader.getResourceAsStream(resourceName))) {
|
||||
final char[] buffer = new char[0x1000];
|
||||
int charsRead = reader.read(buffer);
|
||||
while (charsRead >= 0) {
|
||||
sb.append(buffer, 0, charsRead);
|
||||
charsRead = reader.read(buffer);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
6
src/main/resources/categories.css
Normal file
6
src/main/resources/categories.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.tableFixHead { overflow: auto; height: 100vh; }
|
||||
.tableFixHead thead th { position: sticky; top: 0; z-index: 1; }
|
||||
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { padding: 8px 16px; }
|
||||
th { background: #eee; }
|
||||
11
src/main/resources/categories.js
Normal file
11
src/main/resources/categories.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
function showDetail(h2, categoryId) {
|
||||
var tr = document.body.querySelector("#cat" + categoryId);
|
||||
if(tr.style.display == "none") {
|
||||
tr.style.display = "";
|
||||
h2.style.fontWeight = "bold";
|
||||
}
|
||||
else {
|
||||
tr.style.display = "none";
|
||||
h2.style.fontWeight = "normal";
|
||||
}
|
||||
}
|
||||
10
src/main/resources/categoryDetail.html
Normal file
10
src/main/resources/categoryDetail.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<tr>
|
||||
<td>${source}</td>
|
||||
<td style="padding-left: 20px;">${date}</td>
|
||||
<td style="padding-left: 20px;">${description}</td>
|
||||
<td style="padding-left: 20px;">${extraDescription}</td>
|
||||
<td style="padding-left: 20px; text-align: right;">${amount}</td>
|
||||
<td style="padding-left: 20px;">${regex}</td>
|
||||
<td style="padding-left: 20px;">${flags}</td>
|
||||
<td style="padding-left: 20px;">${requiredSource}</td>
|
||||
</tr>
|
||||
10
src/main/resources/categoryDetailHead.html
Normal file
10
src/main/resources/categoryDetailHead.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Extra Description</th>
|
||||
<th style="text-align: right">Amount</th>
|
||||
<th>Regex</th>
|
||||
<th>Flags</th>
|
||||
<th>Required Source</th>
|
||||
</tr>
|
||||
3
src/main/resources/categoryDetailIntro.html
Normal file
3
src/main/resources/categoryDetailIntro.html
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<tr id="cat${id}" style="display: none">
|
||||
<td style="padding-left: ${indent}; " colspan="15">
|
||||
<table>
|
||||
47
src/main/resources/categoryHead.html
Normal file
47
src/main/resources/categoryHead.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<tr>
|
||||
<td style="padding-left: ${indent}; ">
|
||||
<h2 style="cursor: pointer; font-weight: normal;" onclick="showDetail(this, ${id})">${name}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ">${amount}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ">${total}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-0}">${total-0}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-1}">${total-1}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-2}">${total-2}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-3}">${total-3}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-4}">${total-4}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-5}">${total-5}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-6}">${total-6}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-7}">${total-7}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-8}">${total-8}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-9}">${total-9}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-10}">${total-10}</h2>
|
||||
</td>
|
||||
<td style="padding-left: 15px; text-align: right; ">
|
||||
<h2 style="font-weight: normal; ${color-11}">${total-11}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
2
src/main/resources/getCategories.sql
Normal file
2
src/main/resources/getCategories.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
select id, parent_category_id, name
|
||||
from ${databaseName}.category
|
||||
3
src/main/resources/getTransactionDetail.sql
Normal file
3
src/main/resources/getTransactionDetail.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
select t.id, t.date, t.source, t.description, t.amount, r.category_id, r.regex, r.flags, r.source, r.description
|
||||
from ${databaseName}.transaction t
|
||||
inner join ${regexDatabaseName}.regex r on r.id = t.regex_id
|
||||
17
src/main/resources/grandHead.html
Normal file
17
src/main/resources/grandHead.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Amount</th>
|
||||
<th>Total</th>
|
||||
<th>Jan</th>
|
||||
<th>Feb</th>
|
||||
<th>Mar</th>
|
||||
<th>Apr</th>
|
||||
<th>May</th>
|
||||
<th>Jun</th>
|
||||
<th>Jul</th>
|
||||
<th>Aug</th>
|
||||
<th>Sep</th>
|
||||
<th>Oct</th>
|
||||
<th>Nov</th>
|
||||
<th>Dec</th>
|
||||
</tr>
|
||||
3
src/main/webapp/META-INF/MANIFEST.MF
Normal file
3
src/main/webapp/META-INF/MANIFEST.MF
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Manifest-Version: 1.0
|
||||
Class-Path:
|
||||
|
||||
Loading…
Reference in a new issue