Abstraction for storing binary asset data, and default implementation using java.sql.Blob and Streams.

pull/10/head
Jens Pelzetter 2021-06-22 20:49:41 +02:00
parent 31015b0739
commit c8382b6660
12 changed files with 510 additions and 17 deletions

View File

@ -96,7 +96,8 @@ import java.util.Properties;
) )
}, },
configurations = { configurations = {
org.librecms.CMSConfig.class org.librecms.CMSConfig.class,
org.librecms.assets.BinaryAssetConfig.class
}, },
pageModelComponentModels = { pageModelComponentModels = {
@PageModelComponentModel( @PageModelComponentModel(

View File

@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@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);
}
}
}

View File

@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@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
);
}
}

View File

@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public interface BinaryAssetDataProvider {
InputStream retrieveData(BinaryAsset asset);
void saveData(BinaryAsset asset, InputStream stream);
}

View File

@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
public class BinaryAssetDataService {
@Inject
private ConfigurationManager confManager;
@Inject
@Any
private Instance<BinaryAssetDataProvider> 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<? extends BinaryAssetDataProvider> clazz;
try {
clazz = (Class<? extends BinaryAssetDataProvider>) Class.forName(
config.getBinaryAssetDataProvider()
);
} catch (ClassNotFoundException ex) {
throw new UnexpectedErrorException(ex);
}
final Instance<? extends BinaryAssetDataProvider> 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()
)
);
}
}
}

View File

@ -319,7 +319,6 @@ public class Images {
LOGGER.error(ex); LOGGER.error(ex);
return Response.serverError().build(); return Response.serverError().build();
} }
return Response return Response
.ok(outputStream.toByteArray(), mimeType) .ok(outputStream.toByteArray(), mimeType)
.build(); .build();

View File

@ -48,6 +48,7 @@ public class CmsAssetEditSteps implements MvcAssetEditSteps {
public Set<Class<?>> getResourceClasses() { public Set<Class<?>> getResourceClasses() {
final Set<Class<?>> classes = new HashSet<>(); final Set<Class<?>> classes = new HashSet<>();
classes.add(FileAssetEditStepDownload.class);
classes.add(SideNoteEditStepResources.class); classes.add(SideNoteEditStepResources.class);
return classes; return classes;

View File

@ -24,12 +24,12 @@ import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.libreccm.l10n.GlobalizationHelper; import org.libreccm.l10n.GlobalizationHelper;
import org.libreccm.security.AuthorizationRequired; import org.libreccm.security.AuthorizationRequired;
import org.librecms.assets.BinaryAssetDataService;
import org.librecms.assets.FileAsset; import org.librecms.assets.FileAsset;
import org.librecms.contentsection.AssetRepository; import org.librecms.contentsection.AssetRepository;
import org.librecms.ui.contentsections.AssetPermissionsChecker; import org.librecms.ui.contentsections.AssetPermissionsChecker;
import org.librecms.ui.contentsections.ContentSectionNotFoundException; import org.librecms.ui.contentsections.ContentSectionNotFoundException;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
@ -55,6 +55,7 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
/** /**
* *
@ -84,6 +85,9 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep {
@Inject @Inject
private AssetRepository assetRepo; private AssetRepository assetRepo;
@Inject
private BinaryAssetDataService dataService;
@Inject @Inject
private GlobalizationHelper globalizationHelper; private GlobalizationHelper globalizationHelper;
@ -312,6 +316,9 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep {
} }
} }
@POST @POST
@Path("/upload") @Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA) @Consumes(MediaType.MULTIPART_FORM_DATA)
@ -348,19 +355,10 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep {
.getHeaders(); .getHeaders();
fileName = getFileName(headers); fileName = getFileName(headers);
contentType = getContentType(headers); contentType = getContentType(headers);
final byte[] bytes = new byte[1024]; dataService.saveData(
try (InputStream inputStream = inputPart.getBody( fileAsset,
InputStream.class, null inputPart.getBody(InputStream.class, null)
); );
ByteArrayOutputStream fileDataOutputStream
= new ByteArrayOutputStream()) {
while (inputStream.read(bytes) != -1) {
fileDataOutputStream.writeBytes(bytes);
}
fileAsset.setData(fileDataOutputStream.toByteArray());
}
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.error( LOGGER.error(
"Failed to upload file for FileAsset {}:", assetPath "Failed to upload file for FileAsset {}:", assetPath
@ -370,6 +368,33 @@ public class FileAssetEditStep extends AbstractMvcAssetEditStep {
models.put("uploadFailed", true); models.put("uploadFailed", true);
return buildRedirectPathForStep(); return buildRedirectPathForStep();
} }
// final MultivaluedMap<String, String> 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); fileAsset.setFileName(fileName);

View File

@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@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 {
//
// }
//
// }
}

View File

@ -52,14 +52,19 @@
<h3>#{CmsAssetsStepsDefaultMessagesBundle['fileasset.editstep.file.title']}</h3> <h3>#{CmsAssetsStepsDefaultMessagesBundle['fileasset.editstep.file.title']}</h3>
<c:if test="#{MvcAssetEditStepModel.canEdit}"> <c:if test="#{MvcAssetEditStepModel.canEdit}">
<div class="text-right"> <div class="text-left">
<button class="btn btn-primary" <button class="btn btn-primary"
data-toggle="modal" data-toggle="modal"
data-target="#file-upload-dialog" data-target="#file-upload-dialog"
type="button"> type="button">
<bootstrap:svgIcon icon="upload" /> <bootstrap:svgIcon icon="upload" />
<span class="sr-only">#{CmsAssetsStepsDefaultMessagesBundle['fileasset.editstep.file.upload.button.label']}</span> <span>#{CmsAssetsStepsDefaultMessagesBundle['fileasset.editstep.file.upload.button.label']}</span>
</button> </button>
<a class="btn btn-primary"
href="#{mvc.basePath}/#{ContentSectionModel.sectionName}/assets/#{CmsSelectedAssetModel.assetPath}/@fileasset-edit-download">
<bootstrap:svgIcon icon="download" />
<span>#{CmsAssetsStepsDefaultMessagesBundle['fileasset.editstep.file.download.button.label']}</span>
</a>
</div> </div>
<div aria-hidden="true" <div aria-hidden="true"
aria-labelledby="file-upload-dialog-title" aria-labelledby="file-upload-dialog-title"

View File

@ -387,3 +387,4 @@ createform.externalvideoasset.url.help=The URL of the external video. The video
createform.externalvideoasset.url.label=URL createform.externalvideoasset.url.label=URL
createform.externalvideoasset.description.help=A short description of the contents of the video. createform.externalvideoasset.description.help=A short description of the contents of the video.
createform.externalvideoasset.description.label=Description createform.externalvideoasset.description.label=Description
fileasset.editstep.file.download.button.label=Download File

View File

@ -387,3 +387,4 @@ createform.externalvideoasset.url.help=Die URL der externe Video-Datei. Das Vide
createform.externalvideoasset.url.label=URL createform.externalvideoasset.url.label=URL
createform.externalvideoasset.description.help=Eine kurze Beschreibung des Inhaltes des Videos. createform.externalvideoasset.description.help=Eine kurze Beschreibung des Inhaltes des Videos.
createform.externalvideoasset.description.label=Beschreibung createform.externalvideoasset.description.label=Beschreibung
fileasset.editstep.file.download.button.label=Datei herunterladen