Commit 83c91ea3 authored by R.W.Majeed's avatar R.W.Majeed

report api moved here

parent a483e82f
......@@ -17,9 +17,25 @@ public enum PreferenceKey {
state("local.s"),
country("local.c"),
i2b2Project("i2b2.project"),
/** URI to the i2b2 PM service. Used for authentication and user management */
i2b2ServicePM("i2b2.service.pm"),
/** JNDI data source name which is also used by the i2b2 CRC cell */
i2b2DatasourceCRC("i2b2.datasource.crc"),
rScriptBinary("rscript.binary")
rScriptBinary("rscript.binary"),
/** Location where generated reports are stored together with their preferences */
reportDataPath("report.data.path"),
/** Reports which are no longer needed in the database will be moved to the archiv path. This is a write-only operation. */
reportArchivePath("report.archive.path"),
/** Location where query data will be stored locally */
brokerDataPath("broker.data.path"),
/** Queries which are no longer wanted in the database will be moved to the archiv path. This is a write-only operation. */
brokerArchivePath("broker.archive.path"),
/** Space separated list of brokers to fetch queries from */
brokerEndpointURI("broker.uris"),
/** JNDI datasource name for non-i2b2 tables */
datasource("db.datasource")
;
......
package org.aktin.report;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Period;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.xml.transform.stream.StreamSource;
public abstract class AnnotatedReport implements Report{
private ReportOption<?>[] reportOptions;
private Period defaultPeriod;
private String id;
public AnnotatedReport() throws IllegalArgumentException{
reportOptions = scanAnnotatedOptions();
AnnotatedReport.Report an = getClass().getAnnotation(AnnotatedReport.Report.class);
if( an == null ){
throw new IllegalArgumentException("Must specify the AbstractReport.Report annotation");
}
if( an.defaultPeriod().equals("") ){
defaultPeriod = null;
}else{
defaultPeriod = Period.parse(an.defaultPeriod());
}
if( an.id().length() == 0 ){
this.id = getClass().getCanonicalName();
}else{
this.id = an.id();
}
}
protected StreamSource createStreamSource(URL url){
StreamSource source = new StreamSource();
source.setSystemId(url.toExternalForm());
try {
source.setInputStream(url.openStream());
} catch (IOException e) {
throw new UncheckedIOException("Unable to read export descriptor", e);
}
return source;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report{
/**
* Report id. If not specified, the {@link Class#getCanonicalName()} is used.
* @return
*/
String id() default "";
String displayName();
String description();
String defaultPeriod() default "";
String[] preferences() default {};
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
protected @interface Options{
Option[] value();
}
/**
* Report specific options can be declared at the type level.
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Options.class)
protected @interface Option {
/**
* option's display name. If not specified, the field name is used.
* @return display name
*/
String key();
String displayName() default "";
String description() default "";
/**
* default value. If not specified, the following defaults are used:
* String type will be the empty string {@code ""}. All other types
* will have {@code null} as default.
* @return default value
*/
String defaultValue() default "";
Class<?> type();
}
@SuppressWarnings("rawtypes")
private static class AnnotatedOption implements ReportOption{
private Option option;
public AnnotatedOption(Option opt){
this.option = opt;
}
@Override
public String getDisplayName() {
return option.displayName();
}
@Override
public String getDescription() {
return option.description();
}
@Override
public Class<?> getType() {
return option.type();
}
@Override
public String defaultValue() {
Class<?> c = getType();
if( c == String.class ){
return option.defaultValue();
}else if( option.defaultValue().length() == 0 ){
return null;
}else if( c == Boolean.class ){
return option.defaultValue().toString();
}else{
throw new UnsupportedOperationException("Option of type "+c.getName()+" not implemented");
}
}
@Override
public String getKey() {
return option.key();
}
}
private ReportOption<?>[] scanAnnotatedOptions(){
List<ReportOption<?>> options = new ArrayList<>();
Option[] fields = getClass().getDeclaredAnnotationsByType(Option.class);
for( int i=0; i<fields.length; i++ ){
options.add(new AnnotatedOption(fields[i]));
}
return options.toArray(new ReportOption<?>[options.size()]);
}
@Override
public final String getId(){
return this.id;
}
@Override
public final String getName() {
return getClass().getAnnotation(AnnotatedReport.Report.class).displayName();
}
@Override
public final String getDescription() {
return getClass().getAnnotation(AnnotatedReport.Report.class).description();
}
@Override
public final Period getDefaultPeriod() {
return defaultPeriod;
}
@Override
public final ReportOption<?>[] getConfigurationOptions() {
return reportOptions;
}
@Override
public final String[] getRequiredPreferenceKeys() {
AnnotatedReport.Report an = getClass().getAnnotation(AnnotatedReport.Report.class);
return an.preferences();
}
protected void copyClasspathResources(Path destination, String...names) throws IOException{
Class<?> clazz = getClass();
for( String file : names ){
try( InputStream in = clazz.getResourceAsStream("/"+file) ){
Objects.requireNonNull(in, "File not found in classpath of "+clazz.getName()+": "+file);
Files.copy(in, destination.resolve(file));
}
}
}
}
package org.aktin.report;
import org.aktin.report.GeneratedReport;
/**
* Generated report within persitent storage
* @author R.W.Majeed
*
*/
public interface ArchivedReport extends GeneratedReport {
/**
* Get the id of the generated report within the archive.
* @return unique id
*/
int getId();
}
package org.aktin.report;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Map;
public interface GeneratedReport {
/**
* Media Type (MIME) for the report data.
* @return media type
*/
String getMediaType();
/**
* Physical location of the report data. For the media type, see {@link #getMediaType()}.
* @return path location of the report data.
*/
Path getLocation();
/**
* Start timestamp of the report interval
* @return timestamp
*/
Instant getStartTimestamp();
/**
* End timestamp of the report interval
* @return timestamp
*/
Instant getEndTimestamp();
/**
* Timestamp when the report was generated
* @return timestamp
*/
Instant getDataTimestamp();
/**
* Get the report template id. For the version, see {@link #getTemplateVersion()}.
* @return template id
*/
String getTemplateId();
/**
* Get the version string for the report template indicated by {@link #getTemplateId()}
* @return version string
*/
String getTemplateVersion();
/**
* Preferences used for the generation of the report. The preferences consist of
* key value pairs and include system preferences requested by the report template
* as well as configuration options which can be specified by the user (e.g. display
* configurations).
* @return preference map
*/
Map<String,String> getPreferences();
}
package org.aktin.report;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.time.Period;
import java.util.Arrays;
import javax.xml.transform.Source;
/**
* Interface for reports.
* A report specifies (1) which data should be extracted, (2) one or more R scripts
* to execute with the extracted data and (3) transformation scripts to generate
* PDF or HTML reports.
*
* <p>
* XXX TODO We need a way to pass report configuration/settings to the
* appropriate methods (copyResourcesForFOP, copyResourcesForR). Some
* parameters are common (e.g. organisation, organisational unit) while
* others are only valid for reports (e.g. data extraction start timestamp, end timestamp, current time)
* and some may be report specific (e.g. disable certain parts/plots).
* Maybe use a Map<String,String> for the prototype.
* </p>
*
* TODO move this file to the report-manager project
*
* @author R.W.Majeed
*
*/
public interface Report {
/**
* ID string to differentiate between reports.
* Should contain only filename/URL safe characters.
* <p>
* The default implementation returns the {@link Class#getCanonicalName()}.
* </p>
*
* @return report ID
*/
default String getId(){
return getClass().getCanonicalName();
}
/**
* Version string for the report template.
* @return version string
*/
default String getVersion(){
return getClass().getPackage().getImplementationVersion();
}
/**
* Get the report name
* @return report name
*/
String getName();
/**
* Description
* @return description
*/
String getDescription();
/**
* Default period for the report, relative to the reference date.
* Use only positive values, e.g. 1 Month, 1 Week.
* @return
*/
Period getDefaultPeriod();
/**
* Get a descriptor for the export configuration.
* Can be either an {@code ExportDescriptor} object (see HIStream-export)
* or an {@link InputStream} to an XML representation of the export
* descriptor.
* <p>
* If an {@link InputStream} is returned, the stream must be closed by the
* caller.
*
* </p>
* @return export descriptor as {@link InputStream} or {@code ExportDescriptor}
*/
Source getExportDescriptor();
/**
* Get available configuration options for the report.
* <p>
* The returned options and order should remain the same
* (in terms of {@link Arrays#deepEquals(Object[], Object[])}).
* </p>
* <p>
* XXX this method may be moved to a ReportFactory, since it is basically
* static for the report.
* </p>
* @return report options
*/
ReportOption<?>[] getConfigurationOptions();
/**
* Get required preference keys, which will be used to add
* preference values during report creation.
* <p>The default implementation returns an empty list</p>
*
* @return preference keys
*/
default String[] getRequiredPreferenceKeys(){
return new String[]{};
}
/**
* Copies all scripts and resource needed for the Rscript invocation
* to the specified working directory. The file names of all copied
* resources are returned on exit.
*
* @param workingDirectory working directory for the R invocation.
* @return file names of the copied resources. The first element of the
* returned array is the main script that should be run to generate
* the report resource.
*
* @throws IOException IO error. No files were copied.
*/
String[] copyResourcesForR(Path workingDirectory)throws IOException;
/**
* Copies all transformation scripts needed for the XML-FO transformation
* to generate a PDF report to the specified working directory.
*
*
* @param workingDirectory for the Apache FOP invocation
* @return files names of copied resources. At least two files must
* be returned: First element the XML input file and second element
* is the XSL file.
*
* @throws IOException IO error. No files were copied.
*/
String[] copyResourcesForFOP(Path workingDirectory)throws IOException;
}
package org.aktin.report;
import java.io.IOException;
import org.aktin.report.GeneratedReport;
/**
* Persistent storage for generated reports.
*
* @author R.W.Majeed
*
*/
public interface ReportArchive extends Iterable<ArchivedReport>{
/**
* Add a generated report to the archive. Returns unique id string.
* @param generated report
* @return report id within the archive
* @throws IOException if the report could not be added
*/
ArchivedReport addReport(GeneratedReport report) throws IOException;
/**
* Retrieve a report by it's id
* @param id id of the generated report
* @return report data
*/
ArchivedReport get(int id);
/**
* Calculate the total number of available generated reports.
* @return number of reports
*/
int size();
/** Remove the specified report from the list of accessible reports. The
* report should be moved to a write only archive and is no longer accessible
* via getReports.
* @param reportId report id to delete
*/
void deleteReport(String reportId) throws IOException;
}
package org.aktin.report;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
public interface ReportManager {
Iterable<Report> reports();
Report getReport(String id);
CompletableFuture<? extends GeneratedReport> generateReport(Report report, Instant fromTimestamp, Instant endTimestamp,
Path reportDestination) throws IOException;
}
\ No newline at end of file
package org.aktin.report;
import java.util.Objects;
public interface ReportOption<T>{
String getKey();
String getDisplayName();
String getDescription();
/**
* Type of the option.
* Permitted types are {@link String}, {@link Integer}, {@link Boolean} and {@link Enum}
* @return option type
*/
Class<T> getType();
/**
* Default value for the option. {@code null} is permitted.
* @return
*/
public String defaultValue();
/**
* Default equals implementation. Two options are equal if and only if
* key, type and default value are equal.
* @param other other option
* @return true if this option is equal to the other option, false otherwise
*/
public default <U> boolean equals(ReportOption<U> other){
return getKey().equals(other.getKey())
&& getType().equals(other.getType())
&& Objects.equals(this.defaultValue(), other.defaultValue());
}
/**
* Create a string option
* @param name name
* @param description description
* @param defaultValue default value
* @return option
*/
public static ReportOption<String> newStringOption(String key, String name, String description, String defaultValue){
return new ReportOption<String>() {
@Override
public String getDisplayName() {return name;}
@Override
public String getDescription() {return description;}
@Override
public Class<String> getType() {return String.class;}
@Override
public String defaultValue() {return defaultValue;}
@Override
public String getKey() {return key;}
};
}
public static ReportOption<Boolean> newBooleanOption(String key, String name, String description, Boolean defaultValue){
return new ReportOption<Boolean>() {
@Override
public String getDisplayName() {return name;}
@Override
public String getDescription() {return description;}
@Override
public Class<Boolean> getType() {return Boolean.class;}
@Override
public String defaultValue() {return defaultValue.toString();}
@Override
public String getKey() { return key;}
};
}
}
package org.aktin.report;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Period;
import javax.xml.transform.Source;
import org.junit.Test;
import static org.junit.Assert.*;
@AnnotatedReport.Report(displayName="rname", description="rdesc", defaultPeriod="P1M")
@AnnotatedReport.Option(key="oname1", type=String.class, displayName="oname1", description="odesc1", defaultValue="odef1")
@AnnotatedReport.Option(key="report.optionBoolean", displayName="optionBoolean", type=Boolean.class)
public class TestAnnotatedReport extends AnnotatedReport {
@Test
public void verifyReportName(){
assertEquals("rname", getName());
}
@Test
public void verifyReportDescription(){
assertEquals("rdesc", getDescription());
}
@Test
public void verifyReportDefaultPeriod(){
assertEquals(Period.ofMonths(1), getDefaultPeriod());
}
@Test
@SuppressWarnings("unchecked")
public void verifyOptions() throws Exception{
ReportOption<?>[] options = getConfigurationOptions();
assertEquals(2, options.length);
ReportOption<String> str = null;
ReportOption<Boolean> bool = null;
for( ReportOption<?> opt : options ){
if( opt.getType() == String.class ){
str = (ReportOption<String>)opt;
}else if( opt.getType() == Boolean.class ){
bool = (ReportOption<Boolean>)opt;
}else{
throw new Exception("Unexpected option type "+opt.getType());
}
}
assertNotNull(str);
assertEquals("oname1", str.getDisplayName());
assertNotNull(bool);
assertEquals("optionBoolean", bool.getDisplayName());
}
@Override
public Source getExportDescriptor() {
return createStreamSource(getClass().getResource("/export-descriptor.xml"));
}
@Override
public String[] copyResourcesForR(Path workingDirectory) throws IOException {
return null;
}
@Override
public String[] copyResourcesForFOP(Path workingDirectory) throws IOException {
return null;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment