From c8382b66600da4fb65c0870007fcc478a010357d Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Tue, 22 Jun 2021 20:49:41 +0200 Subject: [PATCH] Abstraction for storing binary asset data, and default implementation using java.sql.Blob and Streams. --- ccm-cms/src/main/java/org/librecms/Cms.java | 3 +- .../assets/BinaryAssetBlobDataProvider.java | 81 +++++++++ .../librecms/assets/BinaryAssetConfig.java | 84 +++++++++ .../assets/BinaryAssetDataProvider.java | 33 ++++ .../assets/BinaryAssetDataService.java | 91 ++++++++++ .../librecms/contentsection/rs/Images.java | 1 - .../assets/CmsAssetEditSteps.java | 1 + .../assets/FileAssetEditStep.java | 51 ++++-- .../assets/FileAssetEditStepDownload.java | 171 ++++++++++++++++++ .../assets/fileasset/edit-fileasset.xhtml | 9 +- .../ui/MvcAssetStepsBundle.properties | 1 + .../ui/MvcAssetStepsBundle_de.properties | 1 + 12 files changed, 510 insertions(+), 17 deletions(-) create mode 100644 ccm-cms/src/main/java/org/librecms/assets/BinaryAssetBlobDataProvider.java create mode 100644 ccm-cms/src/main/java/org/librecms/assets/BinaryAssetConfig.java create mode 100644 ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataProvider.java create mode 100644 ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataService.java create mode 100644 ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStepDownload.java diff --git a/ccm-cms/src/main/java/org/librecms/Cms.java b/ccm-cms/src/main/java/org/librecms/Cms.java index edfc313f6..40764489b 100644 --- a/ccm-cms/src/main/java/org/librecms/Cms.java +++ b/ccm-cms/src/main/java/org/librecms/Cms.java @@ -96,7 +96,8 @@ import java.util.Properties; ) }, configurations = { - org.librecms.CMSConfig.class + org.librecms.CMSConfig.class, + org.librecms.assets.BinaryAssetConfig.class }, pageModelComponentModels = { @PageModelComponentModel( diff --git a/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetBlobDataProvider.java b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetBlobDataProvider.java new file mode 100644 index 000000000..2f12c6059 --- /dev/null +++ b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetBlobDataProvider.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.librecms.assets; + +import org.libreccm.core.UnexpectedErrorException; + +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.Blob; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; + +import javax.annotation.Resource; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.sql.DataSource; + +/** + * + * @author Jens Pelzetter + */ +@Dependent +public class BinaryAssetBlobDataProvider implements BinaryAssetDataProvider { + + @Resource(lookup = "java:/comp/env/jdbc/libreccm/db") + private DataSource dataSource; + + @Override + public InputStream retrieveData(final BinaryAsset asset) { + Objects.requireNonNull(asset, "Can't retrieve data from null."); + try ( Connection connection = dataSource.getConnection()) { + final PreparedStatement stmt = connection + .prepareStatement( + "SELECT data FROM binary_assets WHERE object_id = ?" + ); + stmt.setLong(1, asset.getObjectId()); + + try (ResultSet resultSet = stmt.executeQuery()) { + final Blob blob = resultSet.getBlob("data"); + return blob.getBinaryStream(); + } + } catch (SQLException ex) { + throw new UnexpectedErrorException(ex); + } + } + + @Override + public void saveData(final BinaryAsset asset, final InputStream stream) { + Objects.requireNonNull(asset, "Can't save data to null."); + try ( Connection connection = dataSource.getConnection()) { + final PreparedStatement stmt = connection + .prepareStatement( + "UPDATE binary_assets SET data = ? WHERE object_id = ?" + ); + stmt.setBlob(1, stream); + stmt.setLong(2, asset.getObjectId()); + } catch (SQLException ex) { + throw new UnexpectedErrorException(ex); + } + } + +} diff --git a/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetConfig.java b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetConfig.java new file mode 100644 index 000000000..b8d5683ae --- /dev/null +++ b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetConfig.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.librecms.assets; + +import org.libreccm.configuration.Configuration; +import org.libreccm.configuration.Setting; + +import java.util.Objects; + +/** + * Configuration parameters for binary assets. + * + * @author Jens Pelzetter + */ +@Configuration +public final class BinaryAssetConfig { + + /** + * Sets the implementation of {@link BinaryAssetDataProvider} to use. + */ + @Setting + private String binaryAssetDataProvider = BinaryAssetBlobDataProvider.class + .getName(); + + public String getBinaryAssetDataProvider() { + return binaryAssetDataProvider; + } + + public void setBinaryAssetDataProvider(final String binaryAssetDataProvider) { + this.binaryAssetDataProvider = binaryAssetDataProvider; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 67 * hash + Objects.hashCode(binaryAssetDataProvider); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof BinaryAssetConfig)) { + return false; + } + final BinaryAssetConfig other = (BinaryAssetConfig) obj; + return Objects.equals( + binaryAssetDataProvider, + other.binaryAssetDataProvider + ); + } + + @Override + public String toString() { + return String.format( + "%s{ " + + "binaryAssetDataProvider = %s" + + " }", + binaryAssetDataProvider + ); + } + +} diff --git a/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataProvider.java b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataProvider.java new file mode 100644 index 000000000..ba5b4c8c1 --- /dev/null +++ b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.librecms.assets; + +import java.io.InputStream; + +/** + * + * @author Jens Pelzetter + */ +public interface BinaryAssetDataProvider { + + InputStream retrieveData(BinaryAsset asset); + + void saveData(BinaryAsset asset, InputStream stream); + +} diff --git a/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataService.java b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataService.java new file mode 100644 index 000000000..c7490ba42 --- /dev/null +++ b/ccm-cms/src/main/java/org/librecms/assets/BinaryAssetDataService.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.librecms.assets; + +import org.libreccm.configuration.ConfigurationManager; +import org.libreccm.core.UnexpectedErrorException; + +import java.io.InputStream; +import java.util.Objects; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +public class BinaryAssetDataService { + + @Inject + private ConfigurationManager confManager; + + @Inject + @Any + private Instance dataProvider; + + public InputStream retrieveData(final BinaryAsset asset) { + Objects.requireNonNull(asset, "Can't retrieve data from null."); + + final BinaryAssetDataProvider dataProvider = getDataProvider(); + + return dataProvider.retrieveData(asset); + } + + public void saveData(final BinaryAsset asset, final InputStream stream) { + Objects.requireNonNull(asset, "Can't save data to null."); + + final BinaryAssetDataProvider dataProvider = getDataProvider(); + dataProvider.saveData(asset, stream); + } + + @SuppressWarnings("unchecked") + private BinaryAssetDataProvider getDataProvider() { + final BinaryAssetConfig config = confManager.findConfiguration( + BinaryAssetConfig.class + ); + + final Class clazz; + try { + clazz = (Class) Class.forName( + config.getBinaryAssetDataProvider() + ); + } catch (ClassNotFoundException ex) { + throw new UnexpectedErrorException(ex); + } + + final Instance selectedInstance + = dataProvider.select(clazz); + + if (selectedInstance.isResolvable()) { + return selectedInstance.get(); + } else { + throw new UnexpectedErrorException( + String.format( + "The configured implementation of %s could not be resolved.", + BinaryAssetDataProvider.class.getName() + ) + ); + } + } + +} diff --git a/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java b/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java index 9f0ca3c88..e1c2f6ca1 100644 --- a/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java +++ b/ccm-cms/src/main/java/org/librecms/contentsection/rs/Images.java @@ -319,7 +319,6 @@ public class Images { LOGGER.error(ex); return Response.serverError().build(); } - return Response .ok(outputStream.toByteArray(), mimeType) .build(); diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/CmsAssetEditSteps.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/CmsAssetEditSteps.java index 9aadc15d8..99c7c8e8b 100644 --- a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/CmsAssetEditSteps.java +++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/CmsAssetEditSteps.java @@ -48,6 +48,7 @@ public class CmsAssetEditSteps implements MvcAssetEditSteps { public Set> getResourceClasses() { final Set> classes = new HashSet<>(); + classes.add(FileAssetEditStepDownload.class); classes.add(SideNoteEditStepResources.class); return classes; diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStep.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStep.java index c0b8bb39c..97ad7b98d 100644 --- a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStep.java +++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStep.java @@ -24,12 +24,12 @@ import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.libreccm.l10n.GlobalizationHelper; import org.libreccm.security.AuthorizationRequired; +import org.librecms.assets.BinaryAssetDataService; import org.librecms.assets.FileAsset; import org.librecms.contentsection.AssetRepository; import org.librecms.ui.contentsections.AssetPermissionsChecker; import org.librecms.ui.contentsections.ContentSectionNotFoundException; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -55,6 +55,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; /** * @@ -84,6 +85,9 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep { @Inject private AssetRepository assetRepo; + @Inject + private BinaryAssetDataService dataService; + @Inject private GlobalizationHelper globalizationHelper; @@ -312,6 +316,9 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep { } } + + + @POST @Path("/upload") @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -348,19 +355,10 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep { .getHeaders(); fileName = getFileName(headers); contentType = getContentType(headers); - final byte[] bytes = new byte[1024]; - try (InputStream inputStream = inputPart.getBody( - InputStream.class, null + dataService.saveData( + fileAsset, + inputPart.getBody(InputStream.class, null) ); - ByteArrayOutputStream fileDataOutputStream - = new ByteArrayOutputStream()) { - while (inputStream.read(bytes) != -1) { - fileDataOutputStream.writeBytes(bytes); - } - - fileAsset.setData(fileDataOutputStream.toByteArray()); - } - } catch (IOException ex) { LOGGER.error( "Failed to upload file for FileAsset {}:", assetPath @@ -370,6 +368,33 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep { models.put("uploadFailed", true); return buildRedirectPathForStep(); } + +// final MultivaluedMap headers = inputPart +// .getHeaders(); +// fileName = getFileName(headers); +// contentType = getContentType(headers); +// final byte[] bytes = new byte[1024]; +// try (InputStream inputStream = inputPart.getBody( +// InputStream.class, null +// ); +// ByteArrayOutputStream fileDataOutputStream +// = new ByteArrayOutputStream()) { +// while (inputStream.read(bytes) != -1) { +// fileDataOutputStream.writeBytes(bytes); +// } +// +// fileAsset.setData(fileDataOutputStream.toByteArray()); +// } +// +// } catch (IOException ex) { +// LOGGER.error( +// "Failed to upload file for FileAsset {}:", assetPath +// ); +// LOGGER.error(ex); +// +// models.put("uploadFailed", true); +// return buildRedirectPathForStep(); +// } } fileAsset.setFileName(fileName); diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStepDownload.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStepDownload.java new file mode 100644 index 000000000..8c9438221 --- /dev/null +++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/FileAssetEditStepDownload.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2021 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.librecms.ui.contentsections.assets; + +import org.libreccm.security.AuthorizationRequired; +import org.librecms.assets.BinaryAssetDataService; +import org.librecms.assets.FileAsset; +import org.librecms.contentsection.Asset; +import org.librecms.contentsection.AssetRepository; +import org.librecms.contentsection.ContentSection; +import org.librecms.ui.contentsections.ContentSectionNotFoundException; +import org.librecms.ui.contentsections.ContentSectionsUi; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.mail.internet.ContentDisposition; +import javax.transaction.Transactional; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +@Path(MvcAssetEditSteps.PATH_PREFIX + "fileasset-edit-download") + +public class FileAssetEditStepDownload { + + @Inject + private AssetRepository assetRepo; + + @Inject + private BinaryAssetDataService dataService; + + @Inject + private ContentSectionsUi sectionsUi; + + @GET + @Path("/") + @AuthorizationRequired + @Transactional(Transactional.TxType.REQUIRED) + public Response downloadFile( + @PathParam(MvcAssetEditSteps.SECTION_IDENTIFIER_PATH_PARAM) + final String sectionIdentifier, + @PathParam(MvcAssetEditSteps.ASSET_PATH_PATH_PARAM_NAME) + final String assetPath + ) { + + final ContentSection contentSection = sectionsUi + .findContentSection(sectionIdentifier) + .orElseThrow( + () -> new WebApplicationException( + Response + .status(Response.Status.NOT_FOUND) + .entity( + String.format( + "ContentSection %s not found.", + sectionIdentifier + ) + ).build() + ) + ); + + final Asset asset = assetRepo + .findByPath(contentSection, assetPath) + .orElseThrow( + () -> new WebApplicationException( + Response + .status(Response.Status.NOT_FOUND) + .entity( + String.format( + "No asset for path %s found in section %s.", + assetPath, + contentSection.getLabel() + ) + ) + .build() + ) + ); + + if (!(asset instanceof FileAsset)) { + throw new WebApplicationException( + Response + .status(Response.Status.NOT_FOUND) + .entity( + String.format( + "No asset for path %s found in section %s.", + assetPath, + contentSection.getLabel() + ) + ) + .build() + ); + } + final FileAsset fileAsset = (FileAsset) asset; + try ( InputStream dataInputStream = dataService.retrieveData(fileAsset)) { + + final StreamingOutput output = new StreamingOutput() { + + @Override + public void write(final OutputStream outputStream) + throws IOException, WebApplicationException { + byte[] buffer = new byte[8192]; + int length; + while ((length = dataInputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + } + + }; + + return Response + .ok() + .entity(output) + .header("Content-Type", fileAsset.getMimeType()) + .header( + "Content-Disposition", + String.format( + "attachment; filename=\"%s\"", + fileAsset.getFileName() + ) + ) + .build(); + + } catch (IOException ex) { + throw new WebApplicationException( + ex, + Response.Status.INTERNAL_SERVER_ERROR + ); + } + } + +// private class FileAssetOutput implements StreamingOutput { +// +// @Override +// public void write(final OutputStream outputStream) +// throws IOException, WebApplicationException { +// +// } +// +// } +} diff --git a/ccm-cms/src/main/resources/WEB-INF/views/org/librecms/ui/contentsection/assets/fileasset/edit-fileasset.xhtml b/ccm-cms/src/main/resources/WEB-INF/views/org/librecms/ui/contentsection/assets/fileasset/edit-fileasset.xhtml index 1d1bab73f..ceca43498 100644 --- a/ccm-cms/src/main/resources/WEB-INF/views/org/librecms/ui/contentsection/assets/fileasset/edit-fileasset.xhtml +++ b/ccm-cms/src/main/resources/WEB-INF/views/org/librecms/ui/contentsection/assets/fileasset/edit-fileasset.xhtml @@ -52,14 +52,19 @@

#{CmsAssetsStepsDefaultMessagesBundle['fileasset.editstep.file.title']}

-
+
+ + + #{CmsAssetsStepsDefaultMessagesBundle['fileasset.editstep.file.download.button.label']} +