diff --git a/README.md b/README.md index ada03ec..4b682be 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,13 @@ Monitoring - There is a basic authentication for security, the credentials are: `plume//rocks`. + +File +---- +[Plume File](https://github.com/Coreoz/Plume-file) is available through the library API: +- + +2 files upload endpoints examples were added in FileUploadWs: +- that only accepts png files +- that only accept Excel files + diff --git a/pom.xml b/pom.xml index d90d367..a763709 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ 17 4.3.4 + 3.1.0 @@ -167,6 +168,32 @@ plume-admin-api-log + + com.coreoz + plume-file-core + ${plume-file.version} + + + com.coreoz + plume-file-metadata-database + ${plume-file.version} + + + com.coreoz + plume-file-storage-database + ${plume-file.version} + + + com.coreoz + plume-file-web-download-jersey + ${plume-file.version} + + + com.coreoz + plume-file-web-upload-jersey + ${plume-file.version} + + junit diff --git a/src/main/java/com/coreoz/db/QuerydslGenerator.java b/src/main/java/com/coreoz/db/QuerydslGenerator.java index b974c82..ddc533e 100644 --- a/src/main/java/com/coreoz/db/QuerydslGenerator.java +++ b/src/main/java/com/coreoz/db/QuerydslGenerator.java @@ -4,9 +4,12 @@ import java.sql.SQLException; import java.util.Locale; +import com.coreoz.plume.admin.db.generated.AdminUser; import com.coreoz.plume.conf.guice.GuiceConfModule; import com.coreoz.plume.db.querydsl.generation.IdBeanSerializer; import com.coreoz.plume.db.transaction.TransactionManager; +import com.coreoz.plume.file.db.beans.FileDataQueryDsl; +import com.coreoz.plume.file.db.beans.FileMetadataQuerydsl; import com.google.inject.Guice; import com.google.inject.Injector; import com.querydsl.codegen.EntityType; @@ -25,11 +28,11 @@ /** * Generate Querydsl classes for the database layer. * - * Run the {@link #main()} method from your IDE to regenerate Querydsl classes. + * Run the {@link #main(String...)} method from your IDE to regenerate Querydsl classes. */ @Slf4j public class QuerydslGenerator { - private static final String TABLES_PREFIX = "plm_"; + private static final String TABLES_PREFIX = "SWC_"; public static void main(String... args) { Configuration configuration = new Configuration(SQLTemplates.DEFAULT); @@ -49,9 +52,15 @@ public static void main(String... args) { @Override public String getClassName(String tableName) { // uncomment if you are using plume file -// if("plm_file".equalsIgnoreCase(tableName)) { -// return FileEntityQuerydsl.class.getName(); -// } + if("plm_file_data".equalsIgnoreCase(tableName)) { + return FileDataQueryDsl.class.getName(); + } + if("plm_file".equalsIgnoreCase(tableName)) { + return FileMetadataQuerydsl.class.getName(); + } + if("plm_user".equalsIgnoreCase(tableName)) { + return AdminUser.class.getName(); + } return super.getClassName(tableName.substring(TABLES_PREFIX.length())); } diff --git a/src/main/java/com/coreoz/db/generated/QUserFile.java b/src/main/java/com/coreoz/db/generated/QUserFile.java new file mode 100644 index 0000000..a2dd94e --- /dev/null +++ b/src/main/java/com/coreoz/db/generated/QUserFile.java @@ -0,0 +1,71 @@ +package com.coreoz.db.generated; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + +import com.querydsl.sql.ColumnMetadata; +import java.sql.Types; + + + + +/** + * QUserFile is a Querydsl query type for UserFile + */ +@Generated("com.querydsl.sql.codegen.MetaDataSerializer") +public class QUserFile extends com.querydsl.sql.RelationalPathBase { + + private static final long serialVersionUID = 1022868767; + + public static final QUserFile userFile = new QUserFile("SWC_USER_FILE"); + + public final StringPath excelUniqueName = createString("excelUniqueName"); + + public final StringPath pictureUniqueName = createString("pictureUniqueName"); + + public final NumberPath userId = createNumber("userId", Long.class); + + public final com.querydsl.sql.ForeignKey constraint4 = createForeignKey(userId, "ID"); + + public final com.querydsl.sql.ForeignKey constraint4d = createForeignKey(pictureUniqueName, "UNIQUE_NAME"); + + public final com.querydsl.sql.ForeignKey constraint4d0 = createForeignKey(excelUniqueName, "UNIQUE_NAME"); + + public QUserFile(String variable) { + super(UserFile.class, forVariable(variable), "PLUME_DEMO", "SWC_USER_FILE"); + addMetadata(); + } + + public QUserFile(String variable, String schema, String table) { + super(UserFile.class, forVariable(variable), schema, table); + addMetadata(); + } + + public QUserFile(String variable, String schema) { + super(UserFile.class, forVariable(variable), schema, "SWC_USER_FILE"); + addMetadata(); + } + + public QUserFile(Path path) { + super(path.getType(), path.getMetadata(), "PLUME_DEMO", "SWC_USER_FILE"); + addMetadata(); + } + + public QUserFile(PathMetadata metadata) { + super(UserFile.class, metadata, "PLUME_DEMO", "SWC_USER_FILE"); + addMetadata(); + } + + public void addMetadata() { + addMetadata(excelUniqueName, ColumnMetadata.named("FILE_EXCEL_FILENAME").withIndex(3).ofType(Types.VARCHAR).withSize(255).notNull()); + addMetadata(pictureUniqueName, ColumnMetadata.named("FILE_PICTURE_FILENAME").withIndex(2).ofType(Types.VARCHAR).withSize(255).notNull()); + addMetadata(userId, ColumnMetadata.named("USER_ID").withIndex(1).ofType(Types.BIGINT).withSize(64).notNull()); + } + +} + diff --git a/src/main/java/com/coreoz/db/generated/UserFile.java b/src/main/java/com/coreoz/db/generated/UserFile.java new file mode 100644 index 0000000..9f1625b --- /dev/null +++ b/src/main/java/com/coreoz/db/generated/UserFile.java @@ -0,0 +1,48 @@ +package com.coreoz.db.generated; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.annotation.processing.Generated; +import com.querydsl.sql.Column; + +/** + * UserFile is a Querydsl bean type + */ +@Generated("com.coreoz.plume.db.querydsl.generation.IdBeanSerializer") +public class UserFile { + + @Column("FILE_EXCEL_FILENAME") + private String excelUniqueName; + + @Column("FILE_PICTURE_FILENAME") + private String pictureUniqueName; + + @JsonSerialize(using=com.fasterxml.jackson.databind.ser.std.ToStringSerializer.class) + @Column("USER_ID") + private Long userId; + + public String getExcelUniqueName() { + return excelUniqueName; + } + + public void setExcelUniqueName(String excelUniqueName) { + this.excelUniqueName = excelUniqueName; + } + + public String getPictureUniqueName() { + return pictureUniqueName; + } + + public void setPictureUniqueName(String pictureUniqueName) { + this.pictureUniqueName = pictureUniqueName; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + +} + diff --git a/src/main/java/com/coreoz/guice/ApplicationModule.java b/src/main/java/com/coreoz/guice/ApplicationModule.java index 71e0ff9..32bfb59 100644 --- a/src/main/java/com/coreoz/guice/ApplicationModule.java +++ b/src/main/java/com/coreoz/guice/ApplicationModule.java @@ -9,8 +9,14 @@ import com.coreoz.plume.conf.guice.GuiceConfModule; import com.coreoz.plume.db.guice.DataSourceModule; import com.coreoz.plume.db.querydsl.guice.GuiceQuerydslModule; -import com.coreoz.plume.jersey.monitoring.guice.GuiceJacksonWithMetricsModule; +import com.coreoz.plume.file.filetype.FileTypesProvider; +import com.coreoz.plume.file.guice.GuiceFileDownloadModule; +import com.coreoz.plume.file.guice.GuiceFileMetadataDatabaseModule; +import com.coreoz.plume.file.guice.GuiceFileModule; +import com.coreoz.plume.file.guice.GuiceFileStorageDatabaseModule; import com.coreoz.plume.scheduler.guice.GuiceSchedulerModule; +import com.coreoz.services.file.ShowCaseFileTypesProvider; +import com.coreoz.plume.jersey.monitoring.guice.GuiceJacksonWithMetricsModule; import com.coreoz.webservices.admin.permissions.ProjectAdminPermissionService; import com.google.inject.AbstractModule; import org.glassfish.jersey.server.ResourceConfig; @@ -33,6 +39,13 @@ protected void configure() { // API log configuration install(new GuiceSchedulerModule()); + // Plume File + install(new GuiceFileMetadataDatabaseModule()); + install(new GuiceFileStorageDatabaseModule()); + install(new GuiceFileDownloadModule()); + install(new GuiceFileModule()); + bind(FileTypesProvider.class).to(ShowCaseFileTypesProvider.class); + // database setup for the demo install(new DataSourceModule()); diff --git a/src/main/java/com/coreoz/jersey/JerseyConfigProvider.java b/src/main/java/com/coreoz/jersey/JerseyConfigProvider.java index 4885b20..6e4c7cd 100644 --- a/src/main/java/com/coreoz/jersey/JerseyConfigProvider.java +++ b/src/main/java/com/coreoz/jersey/JerseyConfigProvider.java @@ -5,6 +5,7 @@ import javax.inject.Singleton; import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.process.internal.RequestScoped; import org.glassfish.jersey.server.ResourceConfig; @@ -36,6 +37,8 @@ public JerseyConfigProvider(ObjectMapper objectMapper) { config.packages("com.coreoz.webservices"); // admin web-services config.packages("com.coreoz.plume.admin.webservices"); + // plume file + config.packages("com.coreoz.plume.file.webservices"); // enable to fetch the current user as a web-service parameter config.register(new AbstractBinder() { @Override @@ -54,6 +57,8 @@ protected void configure() { config.register(AdminSecurityFeature.class); // to debug web-service requests // register(LoggingFeature.class); + // To use Multipart for file upload + config.register(MultiPartFeature.class); // java 8 config.register(TimeParamProvider.class); @@ -73,4 +78,4 @@ public ResourceConfig get() { return config; } -} \ No newline at end of file +} diff --git a/src/main/java/com/coreoz/services/file/ShowCaseFileTypesProvider.java b/src/main/java/com/coreoz/services/file/ShowCaseFileTypesProvider.java new file mode 100644 index 0000000..b01bef1 --- /dev/null +++ b/src/main/java/com/coreoz/services/file/ShowCaseFileTypesProvider.java @@ -0,0 +1,14 @@ +package com.coreoz.services.file; + +import com.coreoz.plume.file.filetype.FileTypeDatabase; +import com.coreoz.plume.file.filetype.FileTypesProvider; + +import java.util.Collection; +import java.util.List; + +public class ShowCaseFileTypesProvider implements FileTypesProvider { + @Override + public Collection fileTypesAvailable() { + return List.of(ShowcaseFileType.values()); + } +} diff --git a/src/main/java/com/coreoz/services/file/ShowcaseFileType.java b/src/main/java/com/coreoz/services/file/ShowcaseFileType.java new file mode 100644 index 0000000..5a8f813 --- /dev/null +++ b/src/main/java/com/coreoz/services/file/ShowcaseFileType.java @@ -0,0 +1,31 @@ +package com.coreoz.services.file; + +import com.coreoz.db.generated.QUserFile; +import com.coreoz.plume.file.filetype.FileTypeDatabase; +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.dsl.StringPath; + +public enum ShowcaseFileType implements FileTypeDatabase { + PICTURE(QUserFile.userFile, QUserFile.userFile.pictureUniqueName), + EXCEL(QUserFile.userFile, QUserFile.userFile.excelUniqueName), + ; + + private final EntityPath fileEntity; + private final StringPath joinColumn; + + ShowcaseFileType(EntityPath fileEntity, StringPath joinColumn) { + this.fileEntity = fileEntity; + this.joinColumn = joinColumn; + } + + + @Override + public EntityPath getFileEntity() { + return fileEntity; + } + + @Override + public StringPath getJoinColumn() { + return joinColumn; + } +} diff --git a/src/main/java/com/coreoz/webservices/api/file/FileUploadWs.java b/src/main/java/com/coreoz/webservices/api/file/FileUploadWs.java new file mode 100644 index 0000000..0399c6c --- /dev/null +++ b/src/main/java/com/coreoz/webservices/api/file/FileUploadWs.java @@ -0,0 +1,111 @@ +package com.coreoz.webservices.api.file; + +import com.coreoz.plume.file.FileUploadWebJerseyService; +import com.coreoz.plume.file.services.mimetype.FileMimeTypeDetector; +import com.coreoz.plume.file.validator.FileUploadData; +import com.coreoz.plume.file.validator.FileUploadValidator; +import com.coreoz.plume.jersey.security.permission.PublicApi; +import com.coreoz.services.file.ShowcaseFileType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataParam; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.InputStream; +import java.util.Set; + +@Path("/files") +@Tag(name = "Files", description = "Upload files") +@Consumes({MediaType.MULTIPART_FORM_DATA}) +@Produces(MediaType.APPLICATION_JSON) +@PublicApi +@Singleton +public class FileUploadWs { + private final FileUploadWebJerseyService fileUploadWebJerseyService; + private final FileMimeTypeDetector fileMimeTypeDetector; + + @Inject + public FileUploadWs(FileUploadWebJerseyService fileService, FileMimeTypeDetector fileMimeTypeDetector) { + this.fileUploadWebJerseyService = fileService; + this.fileMimeTypeDetector = fileMimeTypeDetector; + } + + /** + * Example of webservice that only accepts JPEG files + */ + @POST + @Path("/pictures") + @Operation(description = "Upload a file") + public Response uploadPicture( + @Context ContainerRequestContext context, + @FormDataParam("file") FormDataBodyPart fileMetadata, + @FormDataParam("file") InputStream fileData + ) { + FileUploadData fileUploadMetadata = FileUploadValidator.from( + fileMetadata, + fileData, + this.fileMimeTypeDetector + ) + .fileMaxSize(2_000_000) + .fileNameAllowEmpty() + .fileNameMaxLength(255) + .fileImage() + .keepOriginalFileName() + .finish(); + return Response.ok( + this.fileUploadWebJerseyService.add( + ShowcaseFileType.PICTURE, + fileUploadMetadata + ) + ) + .build(); + } + + /** + * Example of webservice that only accepts Excel files + */ + @POST + @Path("/reports") + @Operation(description = "Upload a file") + public Response uploadExcelFile( + @Context ContainerRequestContext context, + @FormDataParam("file") FormDataBodyPart fileMetadata, + @FormDataParam("file") InputStream fileData + ) { + FileUploadData fileUploadMetadata = FileUploadValidator.from( + fileMetadata, + fileData, + this.fileMimeTypeDetector + ) + .fileMaxSize(2_000_000) + .fileNameAllowEmpty() + .fileNameMaxLength(255) + .fileExtensionNotEmpty() + .fileExtensions(Set.of("xls", "xlsx")) + // Or mime type + //.fileTypeNotEmpty() + //.mimeTypes(Set.of( + // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + // "application/vnd.ms-excel" + //)) + .sanitizeFileName() + .finish(); + return Response.ok( + this.fileUploadWebJerseyService.add( + ShowcaseFileType.EXCEL, + fileUploadMetadata + ) + ) + .build(); + } +} diff --git a/src/main/resources/db/migration/V3__setup_plume_file.sql b/src/main/resources/db/migration/V3__setup_plume_file.sql new file mode 100644 index 0000000..258dc47 --- /dev/null +++ b/src/main/resources/db/migration/V3__setup_plume_file.sql @@ -0,0 +1,63 @@ +CREATE TABLE `PLM_FILE` +( + `unique_name` VARCHAR(255) NOT NULL, + `file_type` VARCHAR(255) NOT NULL, + `mime_type` VARCHAR(255) NULL, + `file_size` DECIMAL(19, 0) NULL, + `file_original_name` VARCHAR(255) NULL, + `file_extension` VARCHAR(10) NULL, + `checksum` VARCHAR(255) NULL, + `creation_date` TIMESTAMP NOT NULL, + PRIMARY KEY (`unique_name`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8; + + +CREATE TABLE `PLM_FILE_DATA` +( + `unique_name` VARCHAR(255) NOT NULL PRIMARY KEY, + `data` MEDIUMBLOB NOT NULL, + FOREIGN KEY (unique_name) REFERENCES PLM_FILE (unique_name) +) DEFAULT CHARSET = utf8; + +CREATE TABLE `SWC_USER_FILE` +( + `user_id` bigint(20) NOT NULL, + `file_picture_filename` VARCHAR(255) NOT NULL, + `file_excel_filename` VARCHAR(255) NOT NULL, + FOREIGN KEY (user_id) REFERENCES PLM_USER (id), + FOREIGN KEY (file_picture_filename) REFERENCES PLM_FILE (unique_name), + FOREIGN KEY (file_excel_filename) REFERENCES PLM_FILE (unique_name) +) DEFAULT CHARSET = utf8; + +-- DATA EXAMPLE + +INSERT INTO PLM_FILE +VALUES ('e58ed763-928c-4155-bee9-fdbaaadc15f3.xlsx', + 'EXCEL', + 'application/vnd.ms-excel', + 1, + 'numbers.xlsx', + 'xlsx', + 'random_checksum', + CURRENT_TIME()); + +INSERT INTO PLM_FILE_DATA +VALUES ('e58ed763-928c-4155-bee9-fdbaaadc15f3.xlsx', + RAWTOHEX('Test')); + +INSERT INTO PLM_FILE +VALUES ('b2dfabaa-5171-11ee-be56-0242ac120002.png', 'PICTURE', 'image/png', 1, 'beach.png', 'png', + 'random_checksum', CURRENT_TIME()); + +INSERT INTO PLM_FILE_DATA +VALUES ('b2dfabaa-5171-11ee-be56-0242ac120002.png', + RAWTOHEX('Test')); + +INSERT INTO SWC_USER_FILE +VALUES ( + 1, + 'b2dfabaa-5171-11ee-be56-0242ac120002.png', + 'e58ed763-928c-4155-bee9-fdbaaadc15f3.xlsx' + ); +