diff --git a/build.gradle b/build.gradle index 02ee362..ff8de3c 100644 --- a/build.gradle +++ b/build.gradle @@ -15,11 +15,27 @@ repositories { dependencies { testCompile group: 'junit', name: 'junit', version: '4.12' + // Spark, core HTTP server provider compile group: 'com.sparkjava', name: 'spark-core', version: '2.7.2' + + // Google Guava, various misc. libraries compile group: 'com.google.guava', name: 'guava', version: '23.5-jre' - compile group: 'org.slf4j', name: 'slf4j-simple', version:'1.7.21' - compile group: 'com.jamesmurty.utils', name: 'java-xmlbuilder', version:'1.2' + + // Log4j, logging compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1' + compile group: 'org.slf4j', name: 'slf4j-simple', version:'1.7.21' + + // XMLBuilder, simple, chained methods to build XML + compile group: 'com.jamesmurty.utils', name: 'java-xmlbuilder', version:'1.2' + + // Spring, dependency injection + compile group: 'org.springframework', name: 'spring-context', version: '5.1.3.RELEASE' + testCompile group: 'org.springframework', name: 'spring-test', version: '5.1.3.RELEASE' + + // Hibernate, SQL support + compile group: 'org.hibernate', name: 'hibernate-core', version: '5.4.0.Final' + compile group: 'org.springframework', name: 'spring-orm', version: '5.1.3.RELEASE' + compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.25.2' } jar { diff --git a/src/main/java/com/buttongames/butterfly/hibernate/PersistenceOperation.java b/src/main/java/com/buttongames/butterfly/hibernate/PersistenceOperation.java new file mode 100644 index 0000000..fcc1441 --- /dev/null +++ b/src/main/java/com/buttongames/butterfly/hibernate/PersistenceOperation.java @@ -0,0 +1,11 @@ +package com.buttongames.butterfly.hibernate; + +/** + * Interface to encapsulate a persistence operation (save, update, etc.) in Hibernate. + * @author skogaby (skogabyskogaby@gmail.com) + */ +@FunctionalInterface +public interface PersistenceOperation { + + void call(Object entity); +} diff --git a/src/main/java/com/buttongames/butterfly/hibernate/SQLiteDialect.java b/src/main/java/com/buttongames/butterfly/hibernate/SQLiteDialect.java new file mode 100644 index 0000000..62f132d --- /dev/null +++ b/src/main/java/com/buttongames/butterfly/hibernate/SQLiteDialect.java @@ -0,0 +1,215 @@ +package com.buttongames.butterfly.hibernate; + +/** + * Dialect class for Hibernate to work with SQLite. Found here: + * https://stackoverflow.com/a/21142088 + */ +import java.sql.Types; +import org.hibernate.dialect.Dialect; + +import org.hibernate.dialect.function.AbstractAnsiTrimEmulationFunction; +import org.hibernate.dialect.function.NoArgSQLFunction; +import org.hibernate.dialect.function.SQLFunction; +import org.hibernate.dialect.function.SQLFunctionTemplate; +import org.hibernate.dialect.function.StandardSQLFunction; +import org.hibernate.dialect.function.VarArgsSQLFunction; +import org.hibernate.type.StandardBasicTypes; + +public class SQLiteDialect extends Dialect { + public SQLiteDialect() { + registerColumnType(Types.BIT, "boolean"); + registerColumnType(Types.TINYINT, "tinyint"); + registerColumnType(Types.SMALLINT, "smallint"); + registerColumnType(Types.INTEGER, "integer"); + registerColumnType(Types.BIGINT, "bigint"); + registerColumnType(Types.FLOAT, "float"); + registerColumnType(Types.REAL, "real"); + registerColumnType(Types.DOUBLE, "double"); + registerColumnType(Types.NUMERIC, "numeric($p, $s)"); + registerColumnType(Types.DECIMAL, "decimal"); + registerColumnType(Types.CHAR, "char"); + registerColumnType(Types.VARCHAR, "varchar($l)"); + registerColumnType(Types.LONGVARCHAR, "longvarchar"); + registerColumnType(Types.DATE, "date"); + registerColumnType(Types.TIME, "time"); + registerColumnType(Types.TIMESTAMP, "datetime"); + registerColumnType(Types.BINARY, "blob"); + registerColumnType(Types.VARBINARY, "blob"); + registerColumnType(Types.LONGVARBINARY, "blob"); + registerColumnType(Types.BLOB, "blob"); + registerColumnType(Types.CLOB, "clob"); + registerColumnType(Types.BOOLEAN, "boolean"); + + //registerFunction( "abs", new StandardSQLFunction("abs") ); + registerFunction( "concat", new VarArgsSQLFunction(StandardBasicTypes.STRING, "", "||", "") ); + //registerFunction( "length", new StandardSQLFunction("length", StandardBasicTypes.LONG) ); + //registerFunction( "lower", new StandardSQLFunction("lower") ); + registerFunction( "mod", new SQLFunctionTemplate(StandardBasicTypes.INTEGER, "?1 % ?2" ) ); + registerFunction( "quote", new StandardSQLFunction("quote", StandardBasicTypes.STRING) ); + registerFunction( "random", new NoArgSQLFunction("random", StandardBasicTypes.INTEGER) ); + registerFunction( "round", new StandardSQLFunction("round") ); + registerFunction( "substr", new StandardSQLFunction("substr", StandardBasicTypes.STRING) ); + registerFunction( "substring", new SQLFunctionTemplate( StandardBasicTypes.STRING, "substr(?1, ?2, ?3)" ) ); + registerFunction( "trim", new AbstractAnsiTrimEmulationFunction() { + protected SQLFunction resolveBothSpaceTrimFunction() { + return new SQLFunctionTemplate(StandardBasicTypes.STRING, "trim(?1)"); + } + + protected SQLFunction resolveBothSpaceTrimFromFunction() { + return new SQLFunctionTemplate(StandardBasicTypes.STRING, "trim(?2)"); + } + + protected SQLFunction resolveLeadingSpaceTrimFunction() { + return new SQLFunctionTemplate(StandardBasicTypes.STRING, "ltrim(?1)"); + } + + protected SQLFunction resolveTrailingSpaceTrimFunction() { + return new SQLFunctionTemplate(StandardBasicTypes.STRING, "rtrim(?1)"); + } + + protected SQLFunction resolveBothTrimFunction() { + return new SQLFunctionTemplate(StandardBasicTypes.STRING, "trim(?1, ?2)"); + } + + protected SQLFunction resolveLeadingTrimFunction() { + return new SQLFunctionTemplate(StandardBasicTypes.STRING, "ltrim(?1, ?2)"); + } + + protected SQLFunction resolveTrailingTrimFunction() { + return new SQLFunctionTemplate(StandardBasicTypes.STRING, "rtrim(?1, ?2)"); + } + } ); + //registerFunction( "upper", new StandardSQLFunction("upper") ); + } + + public boolean supportsIdentityColumns() { + return true; + } + + /* + public boolean supportsInsertSelectIdentity() { + return true; // As specify in NHibernate dialect + } + */ + + public boolean hasDataTypeInIdentityColumn() { + return false; // As specify in NHibernate dialect + } + + /* + public String appendIdentitySelectToInsert(String insertString) { + return new StringBuffer(insertString.length()+30). // As specify in NHibernate dialect + append(insertString). + append("; ").append(getIdentitySelectString()). + toString(); + } + */ + + public String getIdentityColumnString() { + // return "integer primary key autoincrement"; + return "integer"; + } + + public String getIdentitySelectString() { + return "select last_insert_rowid()"; + } + + public boolean supportsLimit() { + return true; + } + + public boolean bindLimitParametersInReverseOrder() { + return true; + } + + protected String getLimitString(String query, boolean hasOffset) { + return new StringBuffer(query.length()+20). + append(query). + append(hasOffset ? " limit ? offset ?" : " limit ?"). + toString(); + } + + public boolean supportsTemporaryTables() { + return true; + } + + public String getCreateTemporaryTableString() { + return "create temporary table if not exists"; + } + + public boolean dropTemporaryTableAfterUse() { + return true; // TODO Validate + } + + public boolean supportsCurrentTimestampSelection() { + return true; + } + + public boolean isCurrentTimestampSelectStringCallable() { + return false; + } + + public String getCurrentTimestampSelectString() { + return "select current_timestamp"; + } + + public boolean supportsUnionAll() { + return true; + } + + public boolean hasAlterTable() { + return false; // As specify in NHibernate dialect + } + + public boolean dropConstraints() { + return false; + } + + public String getAddColumnString() { + return "add column"; + } + + public String getForUpdateString() { + return ""; + } + + public boolean supportsOuterJoinForUpdate() { + return false; + } + + public String getDropForeignKeyString() { + throw new UnsupportedOperationException("No drop foreign key syntax supported by SQLiteDialect"); + } + + public String getAddForeignKeyConstraintString(String constraintName, + String[] foreignKey, String referencedTable, String[] primaryKey, + boolean referencesPrimaryKey) { + throw new UnsupportedOperationException("No add foreign key syntax supported by SQLiteDialect"); + } + + public String getAddPrimaryKeyConstraintString(String constraintName) { + throw new UnsupportedOperationException("No add primary key syntax supported by SQLiteDialect"); + } + + public boolean supportsIfExistsBeforeTableName() { + return true; + } + + public boolean supportsCascadeDelete() { + return true; + } + + /* not case insensitive for unicode characters by default (ICU extension needed) + public boolean supportsCaseInsensitiveLike() { + return true; + } + */ + + public boolean supportsTupleDistinctCounts() { + return false; + } + + public String getSelectGUIDString() { + return "select hex(randomblob(16))"; + } +} \ No newline at end of file diff --git a/src/main/java/com/buttongames/butterfly/hibernate/dao/AbstractHibernateDao.java b/src/main/java/com/buttongames/butterfly/hibernate/dao/AbstractHibernateDao.java new file mode 100644 index 0000000..d90082d --- /dev/null +++ b/src/main/java/com/buttongames/butterfly/hibernate/dao/AbstractHibernateDao.java @@ -0,0 +1,108 @@ +package com.buttongames.butterfly.hibernate.dao; + +import com.buttongames.butterfly.hibernate.PersistenceOperation; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; + +import java.io.Serializable; +import java.util.List; + +/** + * The abstract base class for the DAOs, to handle opening/closing sessions and + * transactions, things like that. + * @author skogaby (skogabyskogaby@gmail.com) + */ +@Repository +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +@Component +public abstract class AbstractHibernateDao { + + private Class clazz; + + protected final SessionFactory sessionFactory; + protected Session currentSession; + protected Transaction currentTransaction; + + @Autowired + public AbstractHibernateDao(final SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + public final void setClazz(final Class clazzToSet) { + this.clazz = clazzToSet; + } + + public Session openCurrentSession() { + this.currentSession = this.sessionFactory.openSession(); + return this.currentSession; + } + + public Session openCurrentSessionWithTransaction() { + this.currentSession = this.sessionFactory.openSession(); + this.currentTransaction = this.currentSession.beginTransaction(); + return currentSession; + } + + public void closeCurrentSession() { + this.currentSession.close(); + } + + public void closeCurrentSessionwithTransaction() { + this.currentTransaction.commit(); + this.currentSession.close(); + } + + public T findById(final long id) { + this.openCurrentSession(); + final T entity = this.currentSession.get(this.clazz, id); + this.closeCurrentSession(); + return entity; + } + + public List findAll() { + this.openCurrentSession(); + final List entities = this.currentSession.createQuery("from " + this.clazz.getName()).list(); + this.closeCurrentSession(); + return entities; + } + + public void create(final T... entity) { + this.performMutation(x -> this.currentSession.saveOrUpdate(x), entity); + } + + public T[] update(final T... entity) { + this.performMutation(x -> this.currentSession.saveOrUpdate(x), entity); + return entity; + } + + public void delete(final T... entity) { + this.performMutation(x -> this.currentSession.delete(x), entity); + } + + public void deleteById(final long entityId) { + final T entity = this.findById(entityId); + this.delete(entity); + } + + public void performMutation(final PersistenceOperation operation, final T... entities) { + if (entities != null) { + try { + this.openCurrentSessionWithTransaction(); + + for (int i = 0; i < entities.length; i++) { + operation.call(entities[i]); + } + + this.closeCurrentSessionwithTransaction(); + } catch (Exception e) { + this.currentTransaction.rollback(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/buttongames/butterfly/spring/configuration/ApplicationConfiguration.java b/src/main/java/com/buttongames/butterfly/spring/configuration/ApplicationConfiguration.java new file mode 100644 index 0000000..9e7e34d --- /dev/null +++ b/src/main/java/com/buttongames/butterfly/spring/configuration/ApplicationConfiguration.java @@ -0,0 +1,20 @@ +package com.buttongames.butterfly.spring.configuration; + +import com.buttongames.butterfly.util.PathUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Bean config class for the top-level application. + * @author skogaby (skogabyskogaby@gmail.com) + */ +@Configuration +@ComponentScan({"com.buttongames.butterfly.spring.configuration"}) +public class ApplicationConfiguration { + + @Bean + public PathUtils pathUtils() { + return new PathUtils(); + } +} diff --git a/src/main/java/com/buttongames/butterfly/spring/configuration/HibernateConfiguration.java b/src/main/java/com/buttongames/butterfly/spring/configuration/HibernateConfiguration.java new file mode 100644 index 0000000..ff55543 --- /dev/null +++ b/src/main/java/com/buttongames/butterfly/spring/configuration/HibernateConfiguration.java @@ -0,0 +1,77 @@ +package com.buttongames.butterfly.spring.configuration; + +import com.buttongames.butterfly.util.PathUtils; +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import java.nio.file.Paths; +import java.util.Properties; + +/** + * Spring configuration for the Hibernate beans. + * @author skogaby (skogabyskogaby@gmail.com) + */ +@Configuration +@ComponentScan({"com.buttongames.butterfly.spring.configuration"}) +@PropertySource("classpath:hibernate.properties") +@EnableTransactionManagement +public class HibernateConfiguration { + + /** The name of the sqlite database file */ + private static final String SQLITE_DATABASE = "butterfly.sqlite"; + + @Value("${jdbc.driverClassName}") + private String driverClassName; + + @Value("${jdbc.username}") + private String username; + + @Value("${jdbc.password}") + private String password; + + @Value("${hibernate.hbm2ddl.auto}") + private String hbm2ddl; + + @Value("${hibernate.dialect}") + private String dialect; + + @Value("${hibernate.show_sql}") + private String showSql; + + @Bean + public LocalSessionFactoryBean sessionFactory(DriverManagerDataSource dataSource) { + final Properties hibernateProperties = new Properties(); + hibernateProperties.setProperty("hibernate.hbm2ddl.auto", this.hbm2ddl); + hibernateProperties.setProperty("hibernate.dialect", this.dialect); + hibernateProperties.setProperty("hibernate.show_sql", this.showSql); + + final LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setPackagesToScan("com.fuckmyclassic.model"); + sessionFactory.setHibernateProperties(hibernateProperties); + + return sessionFactory; + } + + @Bean + public DriverManagerDataSource dataSource(PathUtils pathUtils) { + final DriverManagerDataSource source = new DriverManagerDataSource(); + source.setDriverClassName(this.driverClassName); + source.setUsername(this.username); + source.setPassword(this.password); + + // locate the database in the user directory, and replace backslashes with forward slashes so it works on + // Windows correctly, per sqlite-jdbc's spec + source.setUrl(String.format("jdbc:sqlite:%s", + Paths.get(pathUtils.externalDirectory, SQLITE_DATABASE).toString().replace('\\', '/'))); + + return source; + } +} diff --git a/src/main/java/com/buttongames/butterfly/spring/configuration/HttpConfiguration.java b/src/main/java/com/buttongames/butterfly/spring/configuration/HttpConfiguration.java new file mode 100644 index 0000000..d0d899e --- /dev/null +++ b/src/main/java/com/buttongames/butterfly/spring/configuration/HttpConfiguration.java @@ -0,0 +1,12 @@ +package com.buttongames.butterfly.spring.configuration; + +import org.springframework.context.annotation.Configuration; + +/** + * Bean config class for com.buttongames.butterfly.http package. + * @author skogaby (skogabyskogaby@gmail.com) + */ +@Configuration +public class HttpConfiguration { + +} diff --git a/src/main/java/com/buttongames/butterfly/util/CollectionUtils.java b/src/main/java/com/buttongames/butterfly/util/CollectionUtils.java index 034d9a2..ba7bbdd 100644 --- a/src/main/java/com/buttongames/butterfly/util/CollectionUtils.java +++ b/src/main/java/com/buttongames/butterfly/util/CollectionUtils.java @@ -1,9 +1,5 @@ package com.buttongames.butterfly.util; -import com.google.common.io.ByteStreams; - -import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; /** diff --git a/src/main/java/com/buttongames/butterfly/util/Constants.java b/src/main/java/com/buttongames/butterfly/util/Constants.java index 2ce06d3..60fa1ab 100644 --- a/src/main/java/com/buttongames/butterfly/util/Constants.java +++ b/src/main/java/com/buttongames/butterfly/util/Constants.java @@ -6,6 +6,11 @@ package com.buttongames.butterfly.util; */ public class Constants { + /** + * Name of the app. + */ + public static final String APP_NAME = "butterfly"; + /** * Name of the HTTP header that contains the crypto key, if present. */ diff --git a/src/main/java/com/buttongames/butterfly/util/PathUtils.java b/src/main/java/com/buttongames/butterfly/util/PathUtils.java new file mode 100644 index 0000000..aa494eb --- /dev/null +++ b/src/main/java/com/buttongames/butterfly/util/PathUtils.java @@ -0,0 +1,54 @@ +package com.buttongames.butterfly.util; + +import com.buttongames.butterfly.Main; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.nio.file.Paths; + +/** + * Class to abstract away local paths on the computer. Abstracts the OS-specific pathing + * semantics. + * @author skogaby (skogabyskogaby@gmail.com) + */ +@Component +public class PathUtils { + + /** Name of the flag file to designate the program is in nonportable mode */ + public static final String NONPORTABLE_FLAG = "nonportable.flag"; + + /** Whether or not the program is running in portable mode */ + private final boolean portable; + + /** The directory of the running program itself */ + public final String programDirectory; + + /** The directory where we store userdata -- for portable mode, this is the same as internalDirectory */ + public final String externalDirectory; + + @Autowired + public PathUtils() { + // really ugly hack to make sure we get the path of the program itself, and not the path + // of wherever the process was invoked from; we want it to always go to the installation + // directory + // TODO: Test from a JAR, rather than from IDE testing. This should be more robust in the future + this.programDirectory = new File(Main.class.getProtectionDomain().getCodeSource().getLocation().getPath()) + .getParentFile().getParentFile().getParentFile().getParentFile().toPath().toAbsolutePath().toString(); + + // portable mode is enabled if there is a portable.flag file in the main program directory + final File nonportableFlag = new File(Paths.get(programDirectory, NONPORTABLE_FLAG).toString()); + this.portable = nonportableFlag.exists() ? false : true; + + // the external directory is either the user's home/documents directory (nonportable mode) or + // the installation directory (portable mode) + this.externalDirectory = portable ? programDirectory : + Paths.get(FileSystemView.getFileSystemView().getDefaultDirectory().getPath(), + Constants.APP_NAME).toString(); + } + + public boolean isPortable() { + return portable; + } +} diff --git a/src/main/resources/hibernate.properties b/src/main/resources/hibernate.properties new file mode 100644 index 0000000..73d28cf --- /dev/null +++ b/src/main/resources/hibernate.properties @@ -0,0 +1,7 @@ +jdbc.driverClassName = org.sqlite.JDBC +jdbc.username = +jdbc.password = + +hibernate.hbm2ddl.auto = update +hibernate.dialect = com.buttongames.butterfly.hibernate.SQLiteDialect +hibernate.show_sql = false