diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9f7010 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ \ No newline at end of file diff --git a/annotation/build.gradle b/annotation/build.gradle new file mode 100644 index 0000000..d8b291f --- /dev/null +++ b/annotation/build.gradle @@ -0,0 +1,13 @@ +group 'com.faendir' +version 'unspecified' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { +} diff --git a/annotation/src/main/java/com/faendir/acra/annotation/AutoDiscoverView.java b/annotation/src/main/java/com/faendir/acra/annotation/AutoDiscoverView.java new file mode 100644 index 0000000..2f874f2 --- /dev/null +++ b/annotation/src/main/java/com/faendir/acra/annotation/AutoDiscoverView.java @@ -0,0 +1,17 @@ +package com.faendir.acra.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Lukas + * @since 14.05.2017 + */ +@Inherited +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface AutoDiscoverView { +} diff --git a/annotationprocessor/build.gradle b/annotationprocessor/build.gradle new file mode 100644 index 0000000..9d611fb --- /dev/null +++ b/annotationprocessor/build.gradle @@ -0,0 +1,16 @@ +group 'com.faendir' +version 'unspecified' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile 'com.squareup:javapoet:1.9.0' + compile 'com.google.auto.service:auto-service:1.0-rc2' + compile project(':annotation') +} diff --git a/annotationprocessor/src/main/java/com/faendir/acra/processor/AnnotationProcessor.java b/annotationprocessor/src/main/java/com/faendir/acra/processor/AnnotationProcessor.java new file mode 100644 index 0000000..ad17711 --- /dev/null +++ b/annotationprocessor/src/main/java/com/faendir/acra/processor/AnnotationProcessor.java @@ -0,0 +1,66 @@ +package com.faendir.acra.processor; + +import com.faendir.acra.annotation.AutoDiscoverView; +import com.google.auto.service.AutoService; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.WildcardTypeName; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * @author Lukas + * @since 14.05.2017 + */ +@AutoService(Processor.class) +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class AnnotationProcessor extends AbstractProcessor { + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + Set views = roundEnv.getElementsAnnotatedWith(AutoDiscoverView.class); + if (!views.isEmpty()) { + try { + JavaFile.builder("com.faendir.acra.gen", TypeSpec.classBuilder("ViewDefinition") + .addModifiers(Modifier.PUBLIC) + .addMethod(MethodSpec.methodBuilder("getViewClasses") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ParameterizedTypeName.get(ClassName.get(List.class), ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(Object.class)))) + .addCode("return $T.asList($L);", TypeName.get(Arrays.class), views.stream() + .filter(element -> !element.getModifiers().contains(Modifier.ABSTRACT)) + .map(element -> ((TypeElement) element).getQualifiedName().toString() + ".class") + .reduce((s1, s2) -> s1 + ", " + s2).orElse("")) + .build()) + .build()) + .skipJavaLangImports(true) + .indent(" ") + .build() + .writeTo(processingEnv.getFiler()); + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate classes"); + } + } + return true; + } + + @Override + public Set getSupportedAnnotationTypes() { + return Collections.singleton(AutoDiscoverView.class.getName()); + } +} diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 0000000..bba5b8b --- /dev/null +++ b/backend/build.gradle @@ -0,0 +1,46 @@ +buildscript { + ext { + springBootVersion = '1.5.3.RELEASE' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +plugins { + id "net.ltgt.apt" version "0.10" +} +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' +apply plugin: 'war' + +group 'com.faendir' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +dependencies { + compile('org.springframework.boot:spring-boot-starter-data-mongodb') + compile('org.springframework.boot:spring-boot-starter-security') + compile('com.vaadin:vaadin-spring-boot-starter') + compile('org.codeartisans:org.json:20161124') + compile('org.apache.commons:commons-collections4:4.1') + compile('org.ocpsoft.prettytime:prettytime:3.2.7.Final') + compileOnly project(':annotation') + apt project(':annotationprocessor') + providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' +} + +dependencyManagement { + imports { + mavenBom "com.vaadin:vaadin-bom:8.0.6" + } +} + +war { + archiveName = "acra.war" + version = version +} \ No newline at end of file diff --git a/backend/gradlew b/backend/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/src/main/java/com/faendir/acra/BackendApplication.java b/backend/src/main/java/com/faendir/acra/BackendApplication.java new file mode 100644 index 0000000..673c07b --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/BackendApplication.java @@ -0,0 +1,27 @@ +package com.faendir.acra; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; + +import java.security.SecureRandom; + +@SpringBootApplication(exclude = { SecurityAutoConfiguration.class }) +public class BackendApplication extends SpringBootServletInitializer { + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { + return builder.sources(BackendApplication.class); + } + + public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); + } + + @Bean + public SecureRandom secureRandom(){ + return new SecureRandom(); + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/App.java b/backend/src/main/java/com/faendir/acra/data/App.java new file mode 100644 index 0000000..f4880b3 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/App.java @@ -0,0 +1,34 @@ +package com.faendir.acra.data; + +import org.springframework.data.mongodb.core.mapping.Document; + +/** + * @author Lukas + * @since 22.03.2017 + */ +@Document +public class App { + private String id; + private String name; + private String password; + + public App() { + } + + public App(String name, String password) { + this.name = name; + this.password = password; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + + public String getId() { + return id; + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/AppManager.java b/backend/src/main/java/com/faendir/acra/data/AppManager.java new file mode 100644 index 0000000..9d780d7 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/AppManager.java @@ -0,0 +1,38 @@ +package com.faendir.acra.data; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; + +import java.security.SecureRandom; +import java.util.List; + +/** + * @author Lukas + * @since 22.03.2017 + */ +@Component +public class AppManager { + private final SecureRandom secureRandom; + private final AppRepository appRepository; + + @Autowired + public AppManager(SecureRandom secureRandom, AppRepository appRepository) { + this.secureRandom = secureRandom; + this.appRepository = appRepository; + } + + public App createNewApp(String name){ + byte[] bytes = new byte[12]; + secureRandom.nextBytes(bytes); + return appRepository.save(new App(name, Base64Utils.encodeToString(bytes))); + } + + public List getApps(){ + return appRepository.findAll(); + } + + public App getApp(String id){ + return appRepository.findOne(id); + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/AppRepository.java b/backend/src/main/java/com/faendir/acra/data/AppRepository.java new file mode 100644 index 0000000..e103ffe --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/AppRepository.java @@ -0,0 +1,10 @@ +package com.faendir.acra.data; + +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * @author Lukas + * @since 22.03.2017 + */ +public interface AppRepository extends MongoRepository { +} diff --git a/backend/src/main/java/com/faendir/acra/data/Bug.java b/backend/src/main/java/com/faendir/acra/data/Bug.java new file mode 100644 index 0000000..257949f --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/Bug.java @@ -0,0 +1,41 @@ +package com.faendir.acra.data; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * @author Lukas + * @since 13.05.2017 + */ +public class Bug { + private List reports; + private String trace; + private int versionCode; + + public Bug(Report report){ + reports = new ArrayList<>(); + this.trace = report.getContent().getString("STACK_TRACE"); + this.versionCode = report.getContent().getInt("APP_VERSION_CODE"); + } + + public boolean is(Report report){ + return report.getContent().getString("STACK_TRACE").equals(trace) && report.getContent().getInt("APP_VERSION_CODE") == versionCode; + } + + public List getReports() { + return reports; + } + + public Date getLastDate(){ + return reports.stream().map(Report::getDate).reduce((d1, d2) -> d1.after(d2) ? d1 : d2).orElse(new Date()); + } + + public String getTrace() { + return trace; + } + + public int getVersionCode() { + return versionCode; + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/MongoConfiguration.java b/backend/src/main/java/com/faendir/acra/data/MongoConfiguration.java new file mode 100644 index 0000000..a255f33 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/MongoConfiguration.java @@ -0,0 +1,26 @@ +package com.faendir.acra.data; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +/** + * @author Lukas + * @since 13.05.2017 + */ +@Configuration +public class MongoConfiguration { + + @Bean + public MappingMongoConverter mongoConverter(MongoDbFactory mongoFactory, MongoMappingContext mongoMappingContext) throws Exception { + DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoFactory); + MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mongoMappingContext); + mongoConverter.setMapKeyDotReplacement("%&&%"); + + return mongoConverter; + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/Report.java b/backend/src/main/java/com/faendir/acra/data/Report.java new file mode 100644 index 0000000..7b1a243 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/Report.java @@ -0,0 +1,40 @@ +package com.faendir.acra.data; + +import org.json.JSONObject; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; + +/** + * @author Lukas + * @since 22.03.2017 + */ +@Document +public class Report { + private String id; + @Indexed + private String app; + private JSONObject content; + + public Report() { + } + + public Report(JSONObject content, String app) { + id = content == null ? null : content.getString("REPORT_ID"); + this.content = content; + this.app = app; + } + + public JSONObject getContent() { + return content; + } + + public Date getDate() { + return ReportUtils.getDateFromString(content.getString("USER_CRASH_DATE")); + } + + public String getId() { + return id; + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/ReportManager.java b/backend/src/main/java/com/faendir/acra/data/ReportManager.java new file mode 100644 index 0000000..eb676c7 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/ReportManager.java @@ -0,0 +1,35 @@ +package com.faendir.acra.data; + +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Lukas + * @since 22.03.2017 + */ +@Component +public class ReportManager { + private final ReportRepository reportRepository; + + @Autowired + public ReportManager(ReportRepository reportRepository) { + this.reportRepository = reportRepository; + } + + public Report newReport(JSONObject content) { + return reportRepository.save(new Report(content, SecurityContextHolder.getContext().getAuthentication().getName())); + } + + public List getReports(String app) { + return reportRepository.findAll(Example.of(new Report(null, app))); + } + + public Report getReport(String id) { + return reportRepository.findOne(id); + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/ReportRepository.java b/backend/src/main/java/com/faendir/acra/data/ReportRepository.java new file mode 100644 index 0000000..e4f5fc0 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/ReportRepository.java @@ -0,0 +1,10 @@ +package com.faendir.acra.data; + +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * @author Lukas + * @since 22.03.2017 + */ +public interface ReportRepository extends MongoRepository { +} diff --git a/backend/src/main/java/com/faendir/acra/data/ReportUtils.java b/backend/src/main/java/com/faendir/acra/data/ReportUtils.java new file mode 100644 index 0000000..a1ada73 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/data/ReportUtils.java @@ -0,0 +1,37 @@ +package com.faendir.acra.data; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * @author Lukas + * @since 13.05.2017 + */ +public final class ReportUtils { + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.ENGLISH); + + public static List getBugs(List reports) { + List bugs = new ArrayList<>(); + for (Report report : reports) { + bugs.stream().filter(bug -> bug.is(report)).findAny().orElseGet(() -> { + Bug bug = new Bug(report); + bugs.add(bug); + return bug; + }).getReports().add(report); + } + return bugs; + } + + static Date getDateFromString(String s) { + try { + return dateFormat.parse(s); + } catch (ParseException e) { + return new Date(); + } + } + +} diff --git a/backend/src/main/java/com/faendir/acra/security/SecurityInitializer.java b/backend/src/main/java/com/faendir/acra/security/SecurityInitializer.java new file mode 100644 index 0000000..26b549a --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/security/SecurityInitializer.java @@ -0,0 +1,24 @@ +package com.faendir.acra.security; + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +/** + * @author Lukas + * @since 13.05.2017 + */ +public class SecurityInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + @Override + protected Class[] getRootConfigClasses() { + return new Class[0]; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[0]; + } + + @Override + protected String[] getServletMappings() { + return new String[0]; + } +} diff --git a/backend/src/main/java/com/faendir/acra/security/SecurityUtils.java b/backend/src/main/java/com/faendir/acra/security/SecurityUtils.java new file mode 100644 index 0000000..005d2a3 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/security/SecurityUtils.java @@ -0,0 +1,21 @@ +package com.faendir.acra.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +public final class SecurityUtils { + + private SecurityUtils() { + } + + public static boolean isLoggedIn() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && authentication.isAuthenticated(); + } + + public static boolean hasRole(String role) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && authentication.getAuthorities().contains(new SimpleGrantedAuthority(role)); + } +} diff --git a/backend/src/main/java/com/faendir/acra/security/VaadinSessionSecurityContextHolderStrategy.java b/backend/src/main/java/com/faendir/acra/security/VaadinSessionSecurityContextHolderStrategy.java new file mode 100644 index 0000000..a3e1dff --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/security/VaadinSessionSecurityContextHolderStrategy.java @@ -0,0 +1,49 @@ +package com.faendir.acra.security; + +import com.vaadin.server.VaadinService; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; + +import com.vaadin.server.VaadinSession; + +/** + * A custom {@link SecurityContextHolderStrategy} that stores the {@link SecurityContext} in the Vaadin Session. + */ +public class VaadinSessionSecurityContextHolderStrategy implements SecurityContextHolderStrategy { + + @Override + public void clearContext() { + getSession().setAttribute(SecurityContext.class, null); + } + + @Override + public SecurityContext getContext() { + VaadinSession session = getSession(); + SecurityContext context = session.getAttribute(SecurityContext.class); + if (context == null) { + context = createEmptyContext(); + session.setAttribute(SecurityContext.class, context); + } + return context; + } + + @Override + public void setContext(SecurityContext context) { + getSession().setAttribute(SecurityContext.class, context); + } + + @Override + public SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } + + private static VaadinSession getSession() { + VaadinSession session = VaadinSession.getCurrent(); + if (session == null) { + session = new VaadinSession(VaadinService.getCurrent()); + VaadinSession.setCurrent(session); + } + return session; + } +} diff --git a/backend/src/main/java/com/faendir/acra/security/WebSecurityConfig.java b/backend/src/main/java/com/faendir/acra/security/WebSecurityConfig.java new file mode 100644 index 0000000..4321b2a --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/security/WebSecurityConfig.java @@ -0,0 +1,107 @@ +package com.faendir.acra.security; + +import com.faendir.acra.data.App; +import com.faendir.acra.data.AppManager; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/** + * @author Lukas + * @since 22.03.2017 + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + static { + SecurityContextHolder.setStrategyName(VaadinSessionSecurityContextHolderStrategy.class.getName()); + } + + private final String user; + private final String password; + private final AppManager appManager; + private final AuthenticationProvider authenticationProvider = new AuthenticationProvider() { + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (authentication instanceof UsernamePasswordAuthenticationToken) { + if (user.equals(authentication.getName())) { + if (password.equals(authentication.getCredentials())) { + return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AuthorityUtils.createAuthorityList("ROLE_ADMIN")); + } else { + throw new BadCredentialsException("Password mismatch for user " + authentication.getName()); + } + } + App app = appManager.getApp(authentication.getName()); + if (app != null) { + if (app.getPassword().equals(authentication.getCredentials())) { + Authentication auth = new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AuthorityUtils.createAuthorityList("ROLE_REPORTER")); + VaadinSession session = VaadinSession.getCurrent(); + if (session == null) session = new VaadinSession(VaadinService.getCurrent()); + VaadinSession.setCurrent(session); + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(auth); + session.setAttribute(SecurityContext.class, securityContext); + return auth; + } else { + throw new BadCredentialsException("Password mismatch for user " + authentication.getName()); + } + } else { + throw new UsernameNotFoundException("Username " + authentication.getName() + " not found"); + } + } + return null; + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } + }; + + @Autowired + public WebSecurityConfig(@Value("${security.user.name}") String user, @Value("${security.user.password}") String password, AppManager appManager) { + this.user = user; + this.password = password; + this.appManager = appManager; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(authenticationProvider); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .headers().disable() + .httpBasic(); + } + + @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return authenticationManager(); + } +} diff --git a/backend/src/main/java/com/faendir/acra/service/ReportService.java b/backend/src/main/java/com/faendir/acra/service/ReportService.java new file mode 100644 index 0000000..9507e86 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/service/ReportService.java @@ -0,0 +1,33 @@ +package com.faendir.acra.service; + +import com.faendir.acra.data.ReportManager; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +/** + * @author Lukas + * @since 22.03.2017 + */ +@RestController +public class ReportService { + private final ReportManager reportManager; + + @Autowired + public ReportService(ReportManager reportManager) { + this.reportManager = reportManager; + } + + @PreAuthorize("hasRole('REPORTER')") + @RequestMapping(value = "/report", consumes = MediaType.APPLICATION_JSON_VALUE) + public void report(@RequestBody String content) throws IOException { + JSONObject jsonObject = new JSONObject(content); + reportManager.newReport(jsonObject); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/BackendUI.java b/backend/src/main/java/com/faendir/acra/ui/BackendUI.java new file mode 100644 index 0000000..c5a9228 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/BackendUI.java @@ -0,0 +1,84 @@ +package com.faendir.acra.ui; + +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.util.Style; +import com.vaadin.annotations.Theme; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinService; +import com.vaadin.spring.annotation.SpringUI; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Button; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.LoginForm; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * @author Lukas + * @since 22.03.2017 + */ +@SpringUI +@Theme("acratheme") +public class BackendUI extends UI { + private final AuthenticationManager authenticationManager; + private final ApplicationContext applicationContext; + private final VerticalLayout content; + + @Autowired + public BackendUI(AuthenticationManager authenticationManager, ApplicationContext applicationContext) { + this.authenticationManager = authenticationManager; + this.applicationContext = applicationContext; + content = new VerticalLayout(); + content.setSizeFull(); + Style.NO_PADDING.apply(content); + } + + @Override + protected void init(VaadinRequest request) { + if (SecurityUtils.isLoggedIn()) { + showMain(); + } else { + LoginForm loginForm = new LoginForm(); + loginForm.addLoginListener(event -> login(event.getLoginParameter("username"), event.getLoginParameter("password"))); + setContent(loginForm); + } + } + + private boolean login(String username, String password) { + try { + Authentication token = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + VaadinService.reinitializeSession(VaadinService.getCurrentRequest()); + SecurityContextHolder.getContext().setAuthentication(token); + showMain(); + return true; + } catch (AuthenticationException ex) { + return false; + } + } + + private void showMain() { + NavigationManager navigationManager = applicationContext.getBean(NavigationManager.class); + HorizontalLayout header = new HorizontalLayout(new Button("Up", e -> navigationManager.navigateBack())); + Style.apply(header, Style.MARGIN_TOP, Style.MARGIN_LEFT, Style.MARGIN_RIGHT); + VerticalLayout root = new VerticalLayout(header, content); + root.setExpandRatio(content, 1); + root.setSizeFull(); + Style.NO_PADDING.apply(root); + setContent(root); + } + + @UIScope + @Bean + public VerticalLayout mainView() { + return content; + } + +} diff --git a/backend/src/main/java/com/faendir/acra/ui/NavigationManager.java b/backend/src/main/java/com/faendir/acra/ui/NavigationManager.java new file mode 100644 index 0000000..4939e7b --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/NavigationManager.java @@ -0,0 +1,73 @@ +package com.faendir.acra.ui; + +import com.faendir.acra.gen.ViewDefinition; +import com.faendir.acra.ui.view.NamedView; +import com.vaadin.navigator.Navigator; +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewProvider; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @author Lukas + * @since 14.05.2017 + */ +@UIScope +@Component +public class NavigationManager { + private final Navigator navigator; + private final ApplicationContext applicationContext; + private final List> views; + private final List backStack; + + @Autowired + public NavigationManager(UI ui, VerticalLayout mainView, ApplicationContext applicationContext) { + navigator = new Navigator(ui, mainView); + backStack = new ArrayList<>(); + this.applicationContext = applicationContext; + navigator.addProvider(new ViewProvider() { + @Override + public String getViewName(String viewAndParameters) { + String name = viewAndParameters.split("/", 2)[0]; + if (views.stream().map(applicationContext::getBean).map(NamedView.class::cast).map(NamedView::getName).anyMatch(name::equals)) + return name; + return null; + } + + @Override + public View getView(String viewName) { + return views.stream().map(applicationContext::getBean).map(NamedView.class::cast).filter(view -> view.getName().equals(viewName)).findAny().orElse(null); + } + }); + views = ViewDefinition.getViewClasses(); + String target = Optional.ofNullable(ui.getPage().getLocation().getFragment()).orElse("").replace("!", ""); + backStack.add(target); + navigator.navigateTo(target); + } + + public void navigateTo(Class namedView, String contentId) { + String target = applicationContext.getBean(namedView).getName() + (contentId == null ? "" : "/" + contentId); + backStack.add(0, target); + navigator.navigateTo(target); + } + + public void navigateBack() { + if (backStack.size() < 2) { + backStack.set(0, ""); + navigator.navigateTo(""); + } else { + backStack.remove(0); + navigator.navigateTo(backStack.get(0)); + } + } + + +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/AppView.java b/backend/src/main/java/com/faendir/acra/ui/view/AppView.java new file mode 100644 index 0000000..e803d28 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/AppView.java @@ -0,0 +1,73 @@ +package com.faendir.acra.ui.view; + +import com.faendir.acra.data.App; +import com.faendir.acra.data.AppManager; +import com.faendir.acra.data.Report; +import com.faendir.acra.data.ReportManager; +import com.faendir.acra.util.Style; +import com.vaadin.data.ValueProvider; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.shared.ui.ContentMode; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Label; +import com.vaadin.ui.TabSheet; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Lukas + * @since 13.05.2017 + */ +@UIScope +@Component +public class AppView extends NamedView { + + private final AppManager appManager; + private final ReportManager reportManager; + + @Autowired + public AppView(AppManager appManager, ReportManager reportManager) { + this.appManager = appManager; + this.reportManager = reportManager; + } + + private Grid.Column addColumn(Grid grid, ValueProvider valueProvider, String caption) { + Grid.Column column = grid.addColumn(valueProvider); + column.setId(caption); + column.setCaption(caption); + return column; + } + + @Override + public String getName() { + return "app"; + } + + @Override + public void enter(ViewChangeListener.ViewChangeEvent event) { + App app = appManager.getApp(event.getParameters()); + List reportList = reportManager.getReports(app.getId()); + VerticalLayout statistics = new VerticalLayout(new Label("Coming soon")); + statistics.setCaption("Statistics"); + statistics.setSizeFull(); + String location = UI.getCurrent().getPage().getLocation().toASCIIString(); + location = location.substring(0, location.indexOf('#')); + VerticalLayout properties = new VerticalLayout(new Label(String.format("Required ACRA configuration:
formUri = \"%sreport\",
" + + "formUriBasicAuthLogin = \"%s\",
formUriBasicAuthPassword = \"%s\",
httpMethod = HttpSender.Method.POST,
reportType = HttpSender.Type.JSON
", + location, app.getId(), app.getPassword()), ContentMode.HTML)); + properties.setCaption("Properties"); + properties.setSizeFull(); + TabSheet tabSheet = new TabSheet(new BugTab(reportList, getNavigationManager()), new ReportList(reportList, getNavigationManager()), statistics, properties); + tabSheet.setSizeFull(); + VerticalLayout content = new VerticalLayout(tabSheet); + content.setSizeFull(); + Style.apply(content, Style.NO_PADDING, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM); + setCompositionRoot(content); + setSizeFull(); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/BugTab.java b/backend/src/main/java/com/faendir/acra/ui/view/BugTab.java new file mode 100644 index 0000000..eb6f1e8 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/BugTab.java @@ -0,0 +1,46 @@ +package com.faendir.acra.ui.view; + +import com.faendir.acra.data.Bug; +import com.faendir.acra.data.Report; +import com.faendir.acra.data.ReportUtils; +import com.faendir.acra.ui.NavigationManager; +import com.faendir.acra.util.StringUtils; +import com.faendir.acra.util.Style; +import com.vaadin.event.selection.SelectionEvent; +import com.vaadin.ui.CustomComponent; +import com.vaadin.ui.VerticalLayout; + +import java.util.List; + +/** + * @author Lukas + * @since 17.05.2017 + */ +public class BugTab extends CustomComponent { + private final VerticalLayout root; + private final NavigationManager navigationManager; + + public BugTab(List reportList, NavigationManager navigationManager) { + this.navigationManager = navigationManager; + MyGrid bugs = new MyGrid<>(null, ReportUtils.getBugs(reportList)); + bugs.setSizeFull(); + bugs.addColumn(bug -> String.valueOf(bug.getReports().size()), "Reports"); + bugs.addColumn(bug -> StringUtils.distanceFromNowAsString(bug.getLastDate()), "Latest Report"); + bugs.addColumn(bug -> String.valueOf(bug.getVersionCode()), "Version"); + bugs.addColumn(bug -> bug.getTrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1); + bugs.addSelectionListener(this::handleBugSelection); + root = new VerticalLayout(bugs); + Style.NO_PADDING.apply(root); + root.setSizeFull(); + setCompositionRoot(root); + setSizeFull(); + setCaption("Bugs"); + } + + private void handleBugSelection(SelectionEvent e) { + if(root.getComponentCount() == 2){ + root.removeComponent(root.getComponent(1)); + } + e.getFirstSelectedItem().ifPresent(bug -> root.addComponent(new ReportList(bug.getReports(), navigationManager))); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/MyGrid.java b/backend/src/main/java/com/faendir/acra/ui/view/MyGrid.java new file mode 100644 index 0000000..e27e69c --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/MyGrid.java @@ -0,0 +1,23 @@ +package com.faendir.acra.ui.view; + +import com.vaadin.data.ValueProvider; +import com.vaadin.ui.Grid; + +import java.util.Collection; + +/** + * @author Lukas + * @since 14.05.2017 + */ +public class MyGrid extends Grid { + public MyGrid(String caption, Collection items) { + super(caption, items); + } + + public Grid.Column addColumn(ValueProvider valueProvider, String caption) { + Grid.Column column = addColumn(valueProvider); + column.setId(caption); + column.setCaption(caption); + return column; + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/NamedView.java b/backend/src/main/java/com/faendir/acra/ui/view/NamedView.java new file mode 100644 index 0000000..2959b34 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/NamedView.java @@ -0,0 +1,31 @@ +package com.faendir.acra.ui.view; + +import com.faendir.acra.annotation.AutoDiscoverView; +import com.faendir.acra.ui.NavigationManager; +import com.vaadin.navigator.View; +import com.vaadin.ui.CustomComponent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * @author Lukas + * @since 14.05.2017 + */ +@AutoDiscoverView +@Component +public abstract class NamedView extends CustomComponent implements View { + private NavigationManager navigationManager; + + public abstract String getName(); + + NavigationManager getNavigationManager() { + return navigationManager; + } + + @Lazy + @Autowired + public void setNavigationManager(NavigationManager navigationManager) { + this.navigationManager = navigationManager; + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/Overview.java b/backend/src/main/java/com/faendir/acra/ui/view/Overview.java new file mode 100644 index 0000000..c98294a --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/Overview.java @@ -0,0 +1,74 @@ +package com.faendir.acra.ui.view; + +import com.faendir.acra.data.App; +import com.faendir.acra.data.AppManager; +import com.faendir.acra.data.ReportManager; +import com.faendir.acra.util.Style; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Button; +import com.vaadin.ui.Grid; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.ui.components.grid.FooterCell; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Lukas + * @since 23.03.2017 + */ +@UIScope +@Component +public class Overview extends NamedView { + + private final AppManager appManager; + private final ReportManager reportManager; + private MyGrid grid; + + @Autowired + public Overview(AppManager appManager, ReportManager reportManager) { + this.appManager = appManager; + this.reportManager = reportManager; + } + + private void addApp() { + Window window = new Window("New App"); + TextField name = new TextField("Name"); + Button create = new Button("Create"); + create.addClickListener(e -> { + appManager.createNewApp(name.getValue()); + window.close(); + grid.setItems(appManager.getApps()); + + }); + VerticalLayout layout = new VerticalLayout(name, create); + window.setContent(layout); + window.center(); + UI.getCurrent().addWindow(window); + } + + @Override + public void enter(ViewChangeListener.ViewChangeEvent event) { + grid = new MyGrid<>("Apps", appManager.getApps()); + grid.setSizeFull(); + Grid.Column column = grid.addColumn(App::getName, "Name"); + grid.addColumn(app -> String.valueOf(reportManager.getReports(app.getId()).size()), "Reports"); + FooterCell footerCell = grid.appendFooterRow().getCell(column.getId()); + Button add = new Button("New App"); + add.setSizeFull(); + add.addClickListener(e -> addApp()); + footerCell.setComponent(add); + VerticalLayout layout = new VerticalLayout(grid); + Style.apply(layout, Style.NO_PADDING, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM); + setCompositionRoot(layout); + grid.addItemClickListener(e -> getNavigationManager().navigateTo(AppView.class, e.getItem().getId())); + } + + @Override + public String getName() { + return ""; + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/ReportList.java b/backend/src/main/java/com/faendir/acra/ui/view/ReportList.java new file mode 100644 index 0000000..fffedea --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/ReportList.java @@ -0,0 +1,29 @@ +package com.faendir.acra.ui.view; + +import com.faendir.acra.data.Report; +import com.faendir.acra.ui.NavigationManager; +import com.faendir.acra.util.StringUtils; + +import java.util.List; + +/** + * @author Lukas + * @since 14.05.2017 + */ +public class ReportList extends MyGrid { + public ReportList(List reportList, NavigationManager navigationManager) { + super("Reports", reportList); + setSizeFull(); + addColumn(report -> StringUtils.distanceFromNowAsString(report.getDate()), "Date"); + addReportColumn("APP_VERSION_NAME", "App Version"); + addReportColumn( "ANDROID_VERSION", "Android Version"); + addReportColumn("PHONE_MODEL", "Device"); + addColumn(report -> report.getContent().getString("STACK_TRACE").split("\n", 2)[0], "Stacktrace").setExpandRatio(1); + addItemClickListener(e -> navigationManager.navigateTo(ReportView.class, e.getItem().getId())); + } + + + private void addReportColumn(String key, String caption) { + addColumn(report -> report.getContent().getString(key), caption); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java b/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java new file mode 100644 index 0000000..543d997 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java @@ -0,0 +1,80 @@ +package com.faendir.acra.ui.view; + +import com.faendir.acra.data.Report; +import com.faendir.acra.data.ReportManager; +import com.faendir.acra.util.Style; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.shared.ui.ContentMode; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Component; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Panel; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/** + * @author Lukas + * @since 13.05.2017 + */ +@UIScope +@org.springframework.stereotype.Component +public class ReportView extends NamedView { + + private final ReportManager reportManager; + + @Autowired + public ReportView(ReportManager reportManager) { + this.reportManager = reportManager; + } + + private Stream getLayoutForEntry(String key, Object value) { + return Stream.of(new Label(key, ContentMode.PREFORMATTED), getComponentForContent(value)); + } + + private GridLayout getLayoutForMap(Map map) { + GridLayout layout = new GridLayout(2, 1, map.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).flatMap(entry -> getLayoutForEntry(entry.getKey(), entry.getValue())).toArray(Component[]::new)); + layout.setDefaultComponentAlignment(Alignment.MIDDLE_LEFT); + layout.setSpacing(false); + layout.setMargin(false); + return layout; + } + + private Component getComponentForContent(Object value) { + if (value instanceof Map) { + //noinspection unchecked + return getLayoutForMap((Map) value); + } else if (value instanceof List) { + //noinspection unchecked + List values = (List) value; + Map map = new HashMap<>(); + for (int i = 0; i < values.size(); i++) { + map.put(String.valueOf(i), values.get(0)); + } + return getLayoutForMap(map); + } + return new Label(value.toString(), ContentMode.PREFORMATTED); + } + + @Override + public String getName() { + return "report"; + } + + @Override + public void enter(ViewChangeListener.ViewChangeEvent event) { + Report report = reportManager.getReport(event.getParameters()); + Component content = getLayoutForMap(report.getContent().toMap()); + Panel panel = new Panel(content); + panel.setSizeFull(); + Style.apply(this, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM); + setCompositionRoot(panel); + setSizeFull(); + } +} diff --git a/backend/src/main/java/com/faendir/acra/util/StringUtils.java b/backend/src/main/java/com/faendir/acra/util/StringUtils.java new file mode 100644 index 0000000..4a3af7a --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/util/StringUtils.java @@ -0,0 +1,16 @@ +package com.faendir.acra.util; + +import org.ocpsoft.prettytime.PrettyTime; + +import java.util.Date; + +/** + * @author Lukas + * @since 14.05.2017 + */ +public class StringUtils { + + public static String distanceFromNowAsString(Date dateTime){ + return new PrettyTime().format(dateTime); + } +} diff --git a/backend/src/main/java/com/faendir/acra/util/Style.java b/backend/src/main/java/com/faendir/acra/util/Style.java new file mode 100644 index 0000000..5b0b34f --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/util/Style.java @@ -0,0 +1,35 @@ +package com.faendir.acra.util; + +import com.vaadin.ui.Component; + +/** + * @author Lukas + * @since 17.05.2017 + */ +public enum Style { + NO_PADDING("no-padding"), + PADDING_LEFT("padding-left"), + PADDING_TOP("padding-top"), + PADDING_RIGHT("padding-right"), + PADDING_BOTTOM("padding-bottom"), + MARGIN_LEFT("margin-left"), + MARGIN_TOP("margin-top"), + MARGIN_RIGHT("margin-right"), + MARGIN_BOTTOM("margin-bottom"), + BACKGROUND_LIGHT_GRAY("background-light-gray"); + final String name; + + Style(String name) { + this.name = name; + } + + public void apply(Component component) { + component.addStyleName(name); + } + + public static void apply(Component component, Style... styles){ + for (Style style : styles){ + style.apply(component); + } + } +} diff --git a/backend/src/main/resources/VAADIN/themes/acratheme/acratheme.scss b/backend/src/main/resources/VAADIN/themes/acratheme/acratheme.scss new file mode 100644 index 0000000..fbf0111 --- /dev/null +++ b/backend/src/main/resources/VAADIN/themes/acratheme/acratheme.scss @@ -0,0 +1,68 @@ +@import "../valo/valo.scss"; + +@mixin acratheme { + @include valo; + + .v-gridlayout-slot { + box-shadow: inset -1px -1px 0px 0px black; + .v-widget { + display: inline; + padding-bottom: 4px; + pre { + padding-left: 6px; + padding-right: 6px; + display: inline; + } + } + } + .v-gridlayout-slot:last-child { + box-shadow: inset -1px 0px 0px 0px black; + } + .v-gridlayout-slot:nth-last-child(2) { + box-shadow: inset -1px 0px 0px 0px black; + } + + .no-padding { + padding: 0 !important; + } + + $padding: 10px; + + .padding-left { + padding-left: $padding !important; + } + + .padding-top { + padding-top: $padding !important; + } + + .padding-right { + padding-right: $padding !important; + } + + .padding-bottom { + padding-bottom: $padding !important; + } + + $margin: 10px; + + .margin-left { + margin-left: $margin; + } + + .margin-top { + margin-top: $margin; + } + + .margin-right { + margin-right: $margin; + } + + .margin-bottom { + margin-bottom: $margin; + } + + .background-light-gray { + background: lightgray; + } +} \ No newline at end of file diff --git a/backend/src/main/resources/VAADIN/themes/acratheme/addons.scss b/backend/src/main/resources/VAADIN/themes/acratheme/addons.scss new file mode 100644 index 0000000..a5670b7 --- /dev/null +++ b/backend/src/main/resources/VAADIN/themes/acratheme/addons.scss @@ -0,0 +1,7 @@ +/* This file is automatically managed and will be overwritten from time to time. */ +/* Do not manually edit this file. */ + +/* Import and include this mixin into your project theme to include the addon themes */ +@mixin addons { +} + diff --git a/backend/src/main/resources/VAADIN/themes/acratheme/styles.scss b/backend/src/main/resources/VAADIN/themes/acratheme/styles.scss new file mode 100644 index 0000000..1c4749b --- /dev/null +++ b/backend/src/main/resources/VAADIN/themes/acratheme/styles.scss @@ -0,0 +1,5 @@ +@import "addons.scss"; +@import "acratheme.scss"; + +@include addons; +@include acratheme; \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..80762ff --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.data.mongodb.port=27017 +spring.data.mongodb.host=127.0.0.1 + +security.user.name=admin +security.user.password=admin +security.user.role=USER + +server.context-path=/acra \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..06b7a9d --- /dev/null +++ b/build.gradle @@ -0,0 +1,7 @@ +allprojects{ + repositories { + mavenCentral() + } +} + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..da7cadf --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.daemon=true +org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f0fea85 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +include 'backend' +include 'annotation' +include 'annotationprocessor' +