Cleanup file streaming utilities (#559)

* Cleanup file streaming.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Added missing comments.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix typo.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Split utility class.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Dependency cleanup.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Add missing dependency,

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Remove repository api dependency from rest core.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix build and sonar issues.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Remove custom ConstraintViolationException

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* RequestMapping should be public.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix errors.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Removed dead code.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Not null

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix nullpointer.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Code cleanup.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
Kai Zimmermann
2017-07-19 12:43:07 +02:00
committed by GitHub
parent 66feae2756
commit 09b24fa97d
59 changed files with 911 additions and 908 deletions

View File

@@ -10,13 +10,14 @@ package org.eclipse.hawkbit.artifact.repository;
import java.io.InputStream;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import com.mongodb.gridfs.GridFSDBFile;
import com.mongodb.gridfs.GridFSFile;
/**
* A wrapper object for the {@link DbArtifact} object which returns the
* A wrapper object for the {@link AbstractDbArtifact} object which returns the
* {@link InputStream} directly from {@link GridFSDBFile#getInputStream()} which
* retrieves when calling {@link #getFileInputStream()} always a new
* {@link InputStream} and not the same.
@@ -24,7 +25,7 @@ import com.mongodb.gridfs.GridFSFile;
*
*
*/
public class GridFsArtifact extends DbArtifact {
public class GridFsArtifact extends AbstractDbArtifact {
private final GridFSFile dbFile;
@@ -32,6 +33,8 @@ public class GridFsArtifact extends DbArtifact {
* @param dbFile
*/
public GridFsArtifact(final GridFSFile dbFile) {
super(dbFile.getId().toString(), new DbArtifactHash(dbFile.getFilename(), dbFile.getMD5()), dbFile.getLength(),
dbFile.getContentType());
this.dbFile = dbFile;
}

View File

@@ -22,7 +22,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.slf4j.Logger;
@@ -89,7 +89,7 @@ public class MongoDBArtifactStore implements ArtifactRepository {
* @return The DbArtifact object or {@code null} if no file exists.
*/
@Override
public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
public AbstractDbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
GridFSDBFile found = gridFs.findOne(new Query()
.addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(sanitizeTenant(tenant))));
@@ -104,13 +104,13 @@ public class MongoDBArtifactStore implements ArtifactRepository {
}
@Override
public DbArtifact store(final String tenant, final InputStream content, final String filename,
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
final String contentType) {
return store(tenant, content, filename, contentType, null);
}
@Override
public DbArtifact store(final String tenant, final InputStream content, final String filename,
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
final String contentType, final DbArtifactHash hash) {
File tempFile = null;
try {
@@ -155,8 +155,8 @@ public class MongoDBArtifactStore implements ArtifactRepository {
}
private DbArtifact store(final String t, final InputStream content, final String contentType, final OutputStream os,
final File tempFile, final DbArtifactHash hash) {
private AbstractDbArtifact store(final String t, final InputStream content, final String contentType,
final OutputStream os, final File tempFile, final DbArtifactHash hash) {
final GridFsArtifact storedArtifact;
final String tenant = sanitizeTenant(t);
try {
@@ -216,15 +216,16 @@ public class MongoDBArtifactStore implements ArtifactRepository {
}
/**
* Maps a list of {@link GridFSDBFile} to paged list of {@link DbArtifact}s.
* Maps a list of {@link GridFSDBFile} to a list of
* {@link AbstractDbArtifact}s.
*
* @param tenant
* the tenant
* @param dbFiles
* the list of mongoDB gridFs files.
* @return a paged list of artifacts mapped from the given dbFiles
* @return list of artifacts mapped from the given dbFiles
*/
private static List<DbArtifact> map(final List<GridFSDBFile> dbFiles) {
private static List<AbstractDbArtifact> map(final List<GridFSDBFile> dbFiles) {
return dbFiles.stream().map(MongoDBArtifactStore::map).collect(Collectors.toList());
}
@@ -233,14 +234,14 @@ public class MongoDBArtifactStore implements ArtifactRepository {
*
* @param ids
* the ids of the files to lookup.
* @return list of artfiacts
* @return list of artifacts
*/
public List<DbArtifact> getArtifactsByIds(final List<String> ids) {
public List<AbstractDbArtifact> getArtifactsByIds(final List<String> ids) {
return map(gridFs.find(new Query().addCriteria(Criteria.where(ID).in(ids))));
}
/**
* Maps a single {@link GridFSFile} to {@link DbArtifact}.
* Maps a single {@link GridFSFile} to {@link AbstractDbArtifact}.
*
* @param tenant
* the tenant
@@ -252,12 +253,8 @@ public class MongoDBArtifactStore implements ArtifactRepository {
if (fsFile == null) {
return null;
}
final GridFsArtifact artifact = new GridFsArtifact(fsFile);
artifact.setArtifactId(fsFile.getId().toString());
artifact.setSize(fsFile.getLength());
artifact.setContentType(fsFile.getContentType());
artifact.setHashes(new DbArtifactHash(fsFile.getFilename(), fsFile.getMD5()));
return artifact;
return new GridFsArtifact(fsFile);
}
@Override

View File

@@ -10,21 +10,28 @@ package org.eclipse.hawkbit.artifact.repository;
import java.io.InputStream;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.springframework.util.Assert;
import com.amazonaws.services.s3.AmazonS3;
/**
* An {@link DbArtifact} implementation which retrieves the {@link InputStream}
* from the {@link AmazonS3} client.
* An {@link AbstractDbArtifact} implementation which retrieves the
* {@link InputStream} from the {@link AmazonS3} client.
*/
public class S3Artifact extends DbArtifact {
public class S3Artifact extends AbstractDbArtifact {
private final AmazonS3 amazonS3;
private final S3RepositoryProperties s3Properties;
private final String key;
S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String key) {
S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String key,
final String artifactId, final DbArtifactHash hashes, final Long size, final String contentType) {
super(artifactId, hashes, size, contentType);
Assert.notNull(amazonS3, "S3 cannot be null");
Assert.notNull(s3Properties, "Properties cannot be null");
Assert.notNull(key, "Key cannot be null");
this.amazonS3 = amazonS3;
this.s3Properties = s3Properties;
this.key = key;

View File

@@ -20,7 +20,7 @@ import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -77,7 +77,7 @@ public class S3Repository implements ArtifactRepository {
}
@Override
public DbArtifact store(final String tenant, final InputStream content, final String filename,
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
final String contentType) {
return store(tenant, content, filename, contentType, null);
}
@@ -86,7 +86,7 @@ public class S3Repository implements ArtifactRepository {
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5
// is not used security related
@SuppressWarnings("squid:S2070")
public DbArtifact store(final String tenant, final InputStream content, final String filename,
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
final String contentType, final DbArtifactHash hash) {
final MessageDigest mdSHA1;
final MessageDigest mdMD5;
@@ -117,7 +117,7 @@ public class S3Repository implements ArtifactRepository {
}
}
private DbArtifact store(final String tenant, final String sha1Hash16, final String mdMD5Hash16,
private AbstractDbArtifact store(final String tenant, final String sha1Hash16, final String mdMD5Hash16,
final String contentType, final File file, final DbArtifactHash hash) {
final S3Artifact s3Artifact = createS3Artifact(tenant, sha1Hash16, mdMD5Hash16, contentType, file);
checkHashes(s3Artifact, hash);
@@ -145,14 +145,8 @@ public class S3Repository implements ArtifactRepository {
private S3Artifact createS3Artifact(final String tenant, final String sha1Hash, final String mdMD5Hash16,
final String contentType, final File file) {
final String key = objectKey(tenant, sha1Hash);
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key);
s3Artifact.setContentType(contentType);
s3Artifact.setArtifactId(sha1Hash);
s3Artifact.setSize(file.length());
s3Artifact.setContentType(contentType);
s3Artifact.setHashes(new DbArtifactHash(sha1Hash, mdMD5Hash16));
return s3Artifact;
return new S3Artifact(amazonS3, s3Properties, objectKey(tenant, sha1Hash), sha1Hash,
new DbArtifactHash(sha1Hash, mdMD5Hash16), file.length(), contentType);
}
private ObjectMetadata createObjectMetadata(final String mdMD5Hash16, final String contentType, final File file) {
@@ -181,7 +175,7 @@ public class S3Repository implements ArtifactRepository {
}
@Override
public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
public AbstractDbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
final String key = objectKey(tenant, sha1Hash);
LOG.info("Retrieving S3 object from bucket {} and key {}", s3Properties.getBucketName(), key);
@@ -192,17 +186,15 @@ public class S3Repository implements ArtifactRepository {
final ObjectMetadata s3ObjectMetadata = s3Object.getObjectMetadata();
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key);
s3Artifact.setArtifactId(sha1Hash);
s3Artifact.setSize(s3ObjectMetadata.getContentLength());
// the MD5Content is stored in the ETag
s3Artifact.setHashes(new DbArtifactHash(sha1Hash,
BaseEncoding.base16().lowerCase().encode(BaseEncoding.base64().decode(s3ObjectMetadata.getETag()))));
s3Artifact.setContentType(s3ObjectMetadata.getContentType());
return s3Artifact;
return new S3Artifact(amazonS3, s3Properties, key, sha1Hash,
new DbArtifactHash(sha1Hash,
BaseEncoding.base16().lowerCase()
.encode(BaseEncoding.base64().decode(s3ObjectMetadata.getETag()))),
s3ObjectMetadata.getContentLength(), s3ObjectMetadata.getContentType());
}
private static void checkHashes(final DbArtifact artifact, final DbArtifactHash hash) {
private static void checkHashes(final AbstractDbArtifact artifact, final DbArtifactHash hash) {
if (hash == null) {
return;
}

View File

@@ -26,7 +26,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.junit.Before;
import org.junit.Test;
@@ -117,7 +117,7 @@ public class S3RepositoryTest {
when(s3ObjectMetadataMock.getContentType()).thenReturn(knownContentType);
// test
final DbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
final AbstractDbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
// verify
assertThat(artifactBySha1.getArtifactId()).isEqualTo(knownSHA1Hash);
@@ -152,7 +152,7 @@ public class S3RepositoryTest {
when(amazonS3Mock.getObject(s3Properties.getBucketName(), knownSHA1Hash)).thenReturn(null);
// test
final DbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
final AbstractDbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
// verify
assertThat(artifactBySha1NotExists).isNull();

View File

@@ -14,19 +14,24 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.springframework.util.Assert;
import com.google.common.base.Throwables;
/**
* A {@link DbArtifact} implementation which dynamically creates a
* {@link AbstractDbArtifact} implementation which dynamically creates a
* {@link FileInputStream} on calling {@link #getFileInputStream()}.
*/
public class ArtifactFilesystem extends DbArtifact {
public class ArtifactFilesystem extends AbstractDbArtifact {
private final File file;
ArtifactFilesystem(final File file) {
public ArtifactFilesystem(final File file, final String artifactId, final DbArtifactHash hashes, final Long size,
final String contentType) {
super(artifactId, hashes, size, contentType);
Assert.notNull(file, "File cannot be null");
this.file = file;
}

View File

@@ -22,7 +22,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -88,7 +88,7 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
}
final File file = createTempFile();
final DbArtifact artifact = store(content, contentType, hash, mdSHA1, mdMD5, file);
final AbstractDbArtifact artifact = store(content, contentType, hash, mdSHA1, mdMD5, file);
return renameFileToSHA1Naming(tenant, file, artifact);
}
@@ -103,25 +103,22 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
if (!file.exists()) {
return null;
}
final ArtifactFilesystem artifact = new ArtifactFilesystem(file);
artifact.setArtifactId(sha1);
artifact.setHashes(new DbArtifactHash(sha1, null));
artifact.setSize(file.length());
return artifact;
return new ArtifactFilesystem(file, sha1, new DbArtifactHash(sha1, null), file.length(), null);
}
private DbArtifact store(final InputStream content, final String contentType, final DbArtifactHash hash,
private AbstractDbArtifact store(final InputStream content, final String contentType, final DbArtifactHash hash,
final MessageDigest mdSHA1, final MessageDigest mdMD5, final File file) {
final DbArtifact artifact = new DbArtifact();
AbstractDbArtifact artifact;
try (final DigestOutputStream outputstream = openFileOutputStream(file, mdSHA1, mdMD5)) {
final long artifactSize = ByteStreams.copy(content, outputstream);
outputstream.flush();
final String sha1Hash = BaseEncoding.base16().lowerCase().encode(mdSHA1.digest());
final String md5Hash = BaseEncoding.base16().lowerCase().encode(mdMD5.digest());
artifact.setArtifactId(sha1Hash);
artifact.setSize(artifactSize);
artifact.setContentType(contentType);
artifact.setHashes(new DbArtifactHash(sha1Hash, md5Hash));
artifact = new ArtifactFilesystem(file, sha1Hash, new DbArtifactHash(sha1Hash, md5Hash), artifactSize,
contentType);
checkHashes(artifact, hash);
} catch (final IOException e) {
throw new ArtifactStoreException(e.getMessage(), e);
@@ -134,9 +131,11 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
return artifact;
}
private ArtifactFilesystem renameFileToSHA1Naming(final String tenant, final File file, final DbArtifact artifact) {
private ArtifactFilesystem renameFileToSHA1Naming(final String tenant, final File file,
final AbstractDbArtifact artifact) {
final File fileSHA1Naming = getFile(tenant, artifact.getHashes().getSha1());
final ArtifactFilesystem fileSystemArtifact = new ArtifactFilesystem(fileSHA1Naming);
final ArtifactFilesystem fileSystemArtifact = new ArtifactFilesystem(fileSHA1Naming, artifact.getArtifactId(),
artifact.getHashes(), artifact.getSize(), artifact.getContentType());
if (fileSHA1Naming.exists()) {
FileUtils.deleteQuietly(file);
} else {
@@ -150,14 +149,11 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
if (!file.delete()) {
LOG.debug("Could not delete temp file {}", file);
}
fileSystemArtifact.setArtifactId(artifact.getArtifactId());
fileSystemArtifact.setContentType(artifact.getContentType());
fileSystemArtifact.setHashes(artifact.getHashes());
fileSystemArtifact.setSize(artifact.getSize());
return fileSystemArtifact;
}
private DbArtifact checkHashes(final DbArtifact artifact, final DbArtifactHash hash) {
private AbstractDbArtifact checkHashes(final AbstractDbArtifact artifact, final DbArtifactHash hash) {
if (hash == null) {
return artifact;
}

View File

@@ -16,7 +16,7 @@ import java.util.Random;
import org.apache.commons.io.IOUtils;
import org.assertj.core.api.Assertions;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.junit.Test;
import ru.yandex.qatools.allure.annotations.Description;
@@ -52,7 +52,7 @@ public class ArtifactFilesystemRepositoryTest {
final byte[] fileContent = randomBytes();
final ArtifactFilesystem artifact = storeRandomArtifact(fileContent);
final DbArtifact artifactBySha1 = artifactFilesystemRepository.getArtifactBySha1(TENANT,
final AbstractDbArtifact artifactBySha1 = artifactFilesystemRepository.getArtifactBySha1(TENANT,
artifact.getHashes().getSha1());
assertThat(artifactBySha1).isNotNull();
}

View File

@@ -16,6 +16,7 @@ import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.assertj.core.api.Assertions;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.junit.Test;
import ru.yandex.qatools.allure.annotations.Description;
@@ -30,7 +31,8 @@ public class ArtifactFilesystemTest {
@Description("Verifies that an exception is thrown on opening an InputStream when file does not exists")
public void getInputStreamOfNonExistingFileThrowsException() {
final File file = new File("fileWhichTotalDoesNotExists");
final ArtifactFilesystem underTest = new ArtifactFilesystem(file);
final ArtifactFilesystem underTest = new ArtifactFilesystem(file, "fileWhichTotalDoesNotExists",
new DbArtifactHash("1", "2"), 0L, null);
try {
underTest.getFileInputStream();
Assertions.fail("Expected a FileNotFoundException because file does not exists");
@@ -45,7 +47,8 @@ public class ArtifactFilesystemTest {
final File createTempFile = File.createTempFile(ArtifactFilesystemTest.class.getSimpleName(), "");
createTempFile.deleteOnExit();
final ArtifactFilesystem underTest = new ArtifactFilesystem(createTempFile);
final ArtifactFilesystem underTest = new ArtifactFilesystem(createTempFile,
ArtifactFilesystemTest.class.getSimpleName(), new DbArtifactHash("1", "2"), 0L, null);
final byte[] buffer = new byte[1024];
IOUtils.read(underTest.getFileInputStream(), buffer);
}

View File

@@ -12,7 +12,7 @@ import java.io.InputStream;
import javax.validation.constraints.NotNull;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.hibernate.validator.constraints.NotEmpty;
@@ -42,7 +42,7 @@ public interface ArtifactRepository {
* @throws ArtifactStoreException
* in case storing of the artifact was not successful
*/
DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
AbstractDbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
String contentType);
/**
@@ -69,7 +69,7 @@ public interface ArtifactRepository {
* in case {@code hash} is provided and not matching to the
* calculated hashes during storing
*/
DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
AbstractDbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
String contentType, DbArtifactHash hash);
/**
@@ -86,7 +86,7 @@ public interface ArtifactRepository {
void deleteBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
/**
* Retrieves a {@link DbArtifact} from the store by it's SHA1 hash.
* Retrieves a {@link AbstractDbArtifact} from the store by it's SHA1 hash.
*
* @param tenant
* the tenant to store the artifact
@@ -97,7 +97,7 @@ public interface ArtifactRepository {
* @throws MethodNotSupportedException
* if implementation does not support the operation
*/
DbArtifact getArtifactBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
AbstractDbArtifact getArtifactBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
/**
* Deletes all artifacts of given tenant.

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.artifact.repository.model;
import java.io.InputStream;
import org.springframework.util.Assert;
/**
* Database representation of artifact.
*
*/
public abstract class AbstractDbArtifact {
private final String artifactId;
private final DbArtifactHash hashes;
private final long size;
private final String contentType;
protected AbstractDbArtifact(final String artifactId, final DbArtifactHash hashes, final long size,
final String contentType) {
Assert.notNull(artifactId, "Artifact ID cannot be null");
Assert.notNull(hashes, "Hashes cannot be null");
this.artifactId = artifactId;
this.hashes = hashes;
this.size = size;
this.contentType = contentType;
}
/**
* @return ID of the artifact
*/
public String getArtifactId() {
return artifactId;
}
/**
* @return hashes of the artifact
*/
public DbArtifactHash getHashes() {
return hashes;
}
/**
* @return site of the artifact in bytes
*/
public long getSize() {
return size;
}
/**
* @return content-type if known by the repository or <code>null</code>
*/
public String getContentType() {
return contentType;
}
/**
* Creates an {@link InputStream} on this artifact. Caller has to take care
* of closing the stream. Repeatable calls open a new {@link InputStream}.
*
* @return {@link InputStream} to read from artifact.
*/
public abstract InputStream getFileInputStream();
}

View File

@@ -1,79 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.artifact.repository.model;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Database representation of artifact.
*
*/
public class DbArtifact {
private String artifactId;
private DbArtifactHash hashes;
private Long size;
private String contentType;
private InputStream fileInputStream;
private OutputStream fileOutputStream;
public void setArtifactId(final String artifactId) {
this.artifactId = artifactId;
}
public String getArtifactId() {
return artifactId;
}
public DbArtifactHash getHashes() {
return hashes;
}
public void setHashes(final DbArtifactHash hashes) {
this.hashes = hashes;
}
public Long getSize() {
return size;
}
public void setSize(final Long size) {
this.size = size;
}
public void setContentType(final String contentType) {
this.contentType = contentType;
}
public String getContentType() {
return contentType;
}
public void setFileInputStream(final InputStream fileInputStream) {
this.fileInputStream = fileInputStream;
}
public InputStream getFileInputStream() {
return fileInputStream;
}
public OutputStream getFileOutputStream() {
return fileOutputStream;
}
public void setFileOutputStream(final OutputStream fileOutputStream) {
this.fileOutputStream = fileOutputStream;
}
}

View File

@@ -14,9 +14,9 @@ package org.eclipse.hawkbit.artifact.repository.model;
*/
public class DbArtifactHash {
private String sha1;
private final String sha1;
private String md5;
private final String md5;
/**
* Constructor.
@@ -27,19 +27,10 @@ public class DbArtifactHash {
* the md5 hash
*/
public DbArtifactHash(final String sha1, final String md5) {
super();
this.sha1 = sha1;
this.md5 = md5;
}
public void setSha1(final String sha1) {
this.sha1 = sha1;
}
public void setMd5(final String md5) {
this.md5 = md5;
}
public String getSha1() {
return sha1;
}

View File

@@ -8,12 +8,9 @@
*/
package org.eclipse.hawkbit.ddi.rest.resource;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.api.ApiType;
import org.eclipse.hawkbit.api.ArtifactUrlHandler;
import org.eclipse.hawkbit.api.URLPlaceholder;
@@ -35,8 +32,6 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.http.HttpRequest;
import com.google.common.base.Charsets;
/**
* Utility class for the DDI API.
*/
@@ -148,23 +143,4 @@ public final class DataConversionHelper {
return result;
}
static void writeMD5FileResponse(final String fileName, final HttpServletResponse response, final Artifact artifact)
throws IOException {
final StringBuilder builder = new StringBuilder();
builder.append(artifact.getMd5Hash());
builder.append(" ");
builder.append(fileName);
final byte[] content = builder.toString().getBytes(Charsets.US_ASCII);
final StringBuilder header = new StringBuilder();
header.append("attachment;filename=");
header.append(fileName);
header.append(DdiRestConstants.ARTIFACT_MD5_DWNL_SUFFIX);
response.setContentLength(content.length);
response.setHeader("Content-Disposition", header.toString());
response.getOutputStream().write(content, 0, content.length);
}
}

View File

@@ -17,7 +17,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.eclipse.hawkbit.api.ArtifactUrlHandler;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory;
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
@@ -38,6 +38,7 @@ import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.SoftwareModuleNotAssignedToTargetException;
@@ -47,8 +48,9 @@ import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
import org.eclipse.hawkbit.rest.util.HttpUtil;
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
import org.eclipse.hawkbit.rest.util.RestResourceConversionHelper;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.util.IpUtil;
@@ -56,7 +58,10 @@ import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Scope;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.ServletServerHttpRequest;
@@ -80,6 +85,12 @@ public class DdiRootController implements DdiRootControllerRestApi {
private static final Logger LOG = LoggerFactory.getLogger(DdiRootController.class);
private static final String GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET = "given action ({}) is not assigned to given target ({}).";
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private ControllerManagement controllerManagement;
@@ -142,7 +153,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
@PathVariable("controllerId") final String controllerId,
@PathVariable("softwareModuleId") final Long softwareModuleId,
@PathVariable("fileName") final String fileName) {
ResponseEntity<InputStream> result;
final ResponseEntity<InputStream> result;
final Target target = controllerManagement.findByControllerId(controllerId)
.orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId));
@@ -159,19 +170,26 @@ public class DdiRootController implements DdiRootControllerRestApi {
@SuppressWarnings("squid:S3655")
final Artifact artifact = module.getArtifactByFilename(fileName).get();
final DbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
final AbstractDbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
.orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash()));
final String ifMatch = requestResponseContextHolder.getHttpServletRequest().getHeader("If-Match");
if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
final String ifMatch = requestResponseContextHolder.getHttpServletRequest().getHeader(HttpHeaders.IF_MATCH);
if (ifMatch != null && !HttpUtil.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
} else {
final ActionStatus action = checkAndLogDownload(requestResponseContextHolder.getHttpServletRequest(),
target, module.getId());
result = RestResourceConversionHelper.writeFileResponse(artifact,
final Long statusId = action.getId();
result = FileStreamingUtil.writeFileResponse(file, artifact.getFilename(),
artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt() : artifact.getCreatedAt(),
requestResponseContextHolder.getHttpServletResponse(),
requestResponseContextHolder.getHttpServletRequest(), file, controllerManagement,
action.getId());
requestResponseContextHolder.getHttpServletRequest(),
(length, shippedSinceLastEvent, total) -> eventPublisher
.publishEvent(new DownloadProgressEvent(tenantAware.getCurrentTenant(), statusId,
shippedSinceLastEvent, applicationContext.getId())));
}
}
return result;
@@ -218,15 +236,19 @@ public class DdiRootController implements DdiRootControllerRestApi {
return ResponseEntity.notFound().build();
}
final Artifact artifact = module.getArtifactByFilename(fileName)
.orElseThrow(() -> new EntityNotFoundException(Artifact.class, fileName));
try {
DataConversionHelper.writeMD5FileResponse(fileName, requestResponseContextHolder.getHttpServletResponse(),
module.getArtifactByFilename(fileName).get());
FileStreamingUtil.writeMD5FileResponse(requestResponseContextHolder.getHttpServletResponse(),
artifact.getMd5Hash(), fileName);
} catch (final IOException e) {
LOG.error("Failed to stream MD5 File", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return ResponseEntity.ok().build();
}
@Override
@@ -254,11 +276,11 @@ public class DdiRootController implements DdiRootControllerRestApi {
final HandlingType handlingType = action.isForce() ? HandlingType.FORCED : HandlingType.ATTEMPT;
List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
final List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY)
: actionHistoryMessageCount);
DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null
final DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null
: new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs);
final DdiDeploymentBase base = new DdiDeploymentBase(Long.toString(action.getId()),

View File

@@ -112,7 +112,7 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
// test failed If-match
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header("If-Match", "fsjkhgjfdhg"))
.header(HttpHeaders.IF_MATCH, "fsjkhgjfdhg"))
.andExpect(status().isPreconditionFailed());
// test invalid range
@@ -311,6 +311,18 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
assertThat(result.getResponse().getContentAsByteArray())
.isEqualTo(Arrays.copyOfRange(random, 1000, resultLength));
// Start download from file end fails
mvc.perform(
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
.header("Range", "bytes=" + random.length + "-"))
.andExpect(status().isRequestedRangeNotSatisfiable())
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().string("Content-Range", "bytes */" + random.length))
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"));
// multipart download - first 20 bytes in 2 parts
result = mvc
.perform(

View File

@@ -13,7 +13,7 @@ import java.util.Optional;
import java.util.UUID;
import org.eclipse.hawkbit.api.HostnameResolver;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.cache.DownloadArtifactCache;
import org.eclipse.hawkbit.cache.DownloadIdCache;
@@ -188,7 +188,7 @@ public class AmqpAuthenticationMessageHandler extends BaseAmqpService {
return Optional.empty();
}
private static DmfArtifact convertDbArtifact(final DbArtifact dbArtifact) {
private static DmfArtifact convertDbArtifact(final AbstractDbArtifact dbArtifact) {
final DmfArtifact artifact = new DmfArtifact();
artifact.setSize(dbArtifact.getSize());
final DbArtifactHash dbArtifactHash = dbArtifact.getHashes();

View File

@@ -16,11 +16,13 @@ import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.net.URL;
import java.util.Optional;
import org.eclipse.hawkbit.api.HostnameResolver;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.ArtifactFilesystem;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.cache.DownloadIdCache;
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
@@ -160,9 +162,8 @@ public class AmqpControllerAuthenticationTest {
when(artifactManagementMock.findArtifact(ARTIFACT_ID)).thenReturn(Optional.of(testArtifact));
when(artifactManagementMock.findFirstArtifactBySHA1(SHA1)).thenReturn(Optional.of(testArtifact));
final DbArtifact artifact = new DbArtifact();
artifact.setSize(ARTIFACT_SIZE);
artifact.setHashes(new DbArtifactHash(SHA1, "md5 test"));
final AbstractDbArtifact artifact = new ArtifactFilesystem(new File("does not exist"), SHA1,
new DbArtifactHash(SHA1, "md5 test"), ARTIFACT_SIZE, null);
when(artifactManagementMock.loadArtifactBinary(SHA1)).thenReturn(Optional.of(artifact));
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate,

View File

@@ -17,6 +17,7 @@ import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
@@ -26,7 +27,9 @@ import java.util.UUID;
import org.eclipse.hawkbit.api.ArtifactUrl;
import org.eclipse.hawkbit.api.ArtifactUrlHandler;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.ArtifactFilesystem;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
@@ -173,9 +176,10 @@ public class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest {
public void testSendDownloadRequest() {
DistributionSet dsA = testdataFactory.createDistributionSet(UUID.randomUUID().toString());
SoftwareModule module = dsA.getModules().iterator().next();
final List<DbArtifact> receivedList = new ArrayList<>();
final List<AbstractDbArtifact> receivedList = new ArrayList<>();
for (final Artifact artifact : testdataFactory.createArtifacts(module.getId())) {
receivedList.add(new DbArtifact());
receivedList.add(new ArtifactFilesystem(new File("./test"), artifact.getSha1Hash(),
new DbArtifactHash(artifact.getSha1Hash(), null), artifact.getSize(), null));
}
module = softwareModuleManagement.findSoftwareModuleById(module.getId()).get();
dsA = distributionSetManagement.findDistributionSetById(dsA.getId()).get();

View File

@@ -28,7 +28,7 @@ import java.util.Optional;
import org.eclipse.hawkbit.api.HostnameResolver;
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.cache.DownloadIdCache;
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
@@ -371,7 +371,7 @@ public class AmqpMessageHandlerServiceTest {
final Artifact localArtifactMock = mock(Artifact.class);
when(localArtifactMock.getSha1Hash()).thenReturn(SHA1);
final DbArtifact dbArtifactMock = mock(DbArtifact.class);
final AbstractDbArtifact dbArtifactMock = mock(AbstractDbArtifact.class);
when(artifactManagementMock.findFirstArtifactBySHA1(SHA1)).thenReturn(Optional.of(localArtifactMock));
when(controllerManagementMock.hasTargetArtifactAssigned(securityToken.getControllerId(), SHA1))
.thenReturn(true);

View File

@@ -8,6 +8,8 @@
*/
package org.eclipse.hawkbit.mgmt.rest.api;
import java.io.InputStream;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
@@ -37,7 +39,7 @@ public interface MgmtDownloadRestApi {
*/
@RequestMapping(method = RequestMethod.GET, value = MgmtRestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING)
@ResponseBody
ResponseEntity<Void> downloadArtifactByDownloadId(@PathVariable("tenant") String tenant,
ResponseEntity<InputStream> downloadArtifactByDownloadId(@PathVariable("tenant") String tenant,
@PathVariable("downloadId") String downloadId);
}

View File

@@ -12,7 +12,7 @@ import java.io.InputStream;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadArtifactRestApi;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
@@ -20,14 +20,17 @@ import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
import org.eclipse.hawkbit.rest.util.HttpUtil;
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
import org.eclipse.hawkbit.rest.util.RestResourceConversionHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
@@ -37,7 +40,6 @@ import org.springframework.web.context.WebApplicationContext;
@RestController
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class MgmtDownloadArtifactResource implements MgmtDownloadArtifactRestApi {
@Autowired
private SoftwareModuleManagement softwareModuleManagement;
@@ -58,25 +60,25 @@ public class MgmtDownloadArtifactResource implements MgmtDownloadArtifactRestApi
* @return responseEntity with status ok if successful
*/
@Override
@ResponseBody
public ResponseEntity<InputStream> downloadArtifact(@PathVariable("softwareModuleId") final Long softwareModuleId,
@PathVariable("artifactId") final Long artifactId) {
final SoftwareModule module = softwareModuleManagement.findSoftwareModuleById(softwareModuleId)
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
final Artifact artifact = module.getArtifact(artifactId)
.orElseThrow(() -> new EntityNotFoundException(Artifact.class, artifactId));
final DbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
final AbstractDbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
.orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash()));
final HttpServletRequest request = requestResponseContextHolder.getHttpServletRequest();
final String ifMatch = request.getHeader("If-Match");
if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
final String ifMatch = request.getHeader(HttpHeaders.IF_MATCH);
if (ifMatch != null && !HttpUtil.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
}
return RestResourceConversionHelper.writeFileResponse(artifact,
requestResponseContextHolder.getHttpServletResponse(), request, file);
return FileStreamingUtil.writeFileResponse(file, artifact.getFilename(),
artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt() : artifact.getCreatedAt(),
requestResponseContextHolder.getHttpServletResponse(), request, null);
}
}

View File

@@ -8,33 +8,26 @@
*/
package org.eclipse.hawkbit.mgmt.rest.resource;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.cache.DownloadArtifactCache;
import org.eclipse.hawkbit.cache.DownloadIdCache;
import org.eclipse.hawkbit.cache.DownloadType;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadRestApi;
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;
/**
* A resource for download artifacts.
*/
@@ -55,8 +48,9 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
@Override
@ResponseBody
public ResponseEntity<Void> downloadArtifactByDownloadId(@PathVariable("tenant") final String tenant,
public ResponseEntity<InputStream> downloadArtifactByDownloadId(@PathVariable("tenant") final String tenant,
@PathVariable("downloadId") final String downloadId) {
try {
final DownloadArtifactCache artifactCache = downloadIdCache.get(downloadId);
if (artifactCache == null) {
@@ -64,7 +58,7 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
return ResponseEntity.notFound().build();
}
DbArtifact artifact = null;
AbstractDbArtifact artifact = null;
if (DownloadType.BY_SHA1.equals(artifactCache.getDownloadType())) {
artifact = artifactRepository.getArtifactBySha1(tenant, artifactCache.getId());
@@ -78,24 +72,12 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
return ResponseEntity.notFound().build();
}
final HttpServletResponse response = requestResponseContextHolder.getHttpServletResponse();
final String etag = artifact.getHashes().getSha1();
final long length = artifact.getSize();
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + downloadId);
response.setHeader(HttpHeaders.ETAG, etag);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setContentLengthLong(length);
return FileStreamingUtil.writeFileResponse(artifact, downloadId, null,
requestResponseContextHolder.getHttpServletResponse(),
requestResponseContextHolder.getHttpServletRequest(), null);
try (InputStream inputstream = artifact.getFileInputStream()) {
ByteStreams.copy(inputstream, requestResponseContextHolder.getHttpServletResponse().getOutputStream());
} catch (final IOException e) {
LOGGER.error("Cannot copy streams", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
} finally {
downloadIdCache.evict(downloadId);
}
return ResponseEntity.ok().build();
}
}

View File

@@ -11,6 +11,8 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.ValidationException;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBody;
@@ -26,7 +28,6 @@ import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
@@ -122,7 +123,7 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
rolloutGroupConditions);
} else {
throw new ConstraintViolationException("Either 'amountGroups' or 'groups' must be defined in the request");
throw new ValidationException("Either 'amountGroups' or 'groups' must be defined in the request");
}
return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(rollout, true));

View File

@@ -13,6 +13,8 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.validation.ValidationException;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
@@ -29,7 +31,6 @@ import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
@@ -293,7 +294,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
if (!MgmtActionType.FORCED.equals(actionUpdate.getForceType())) {
throw new ConstraintViolationException("Resource supports only switch to FORCED.");
throw new ValidationException("Resource supports only switch to FORCED.");
}
action = deploymentManagement.forceTargetAction(actionId);

View File

@@ -32,12 +32,13 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolationException;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.repository.ActionFields;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;

View File

@@ -13,7 +13,7 @@ import java.util.Optional;
import javax.validation.constraints.NotNull;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException;
import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException;
@@ -196,15 +196,15 @@ public interface ArtifactManagement {
Page<Artifact> findArtifactBySoftwareModule(@NotNull Pageable pageReq, @NotNull Long swId);
/**
* Loads {@link DbArtifact} from store for given {@link Artifact}.
* Loads {@link AbstractDbArtifact} from store for given {@link Artifact}.
*
* @param sha1Hash
* to search for
* @return loaded {@link DbArtifact}
* @return loaded {@link AbstractDbArtifact}
*
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_DOWNLOAD_ARTIFACT + SpringEvalExpressions.HAS_AUTH_OR
+ SpringEvalExpressions.HAS_CONTROLLER_DOWNLOAD)
Optional<DbArtifact> loadArtifactBinary(@NotEmpty String sha1Hash);
Optional<AbstractDbArtifact> loadArtifactBinary(@NotEmpty String sha1Hash);
}

View File

@@ -17,7 +17,6 @@ import javax.validation.constraints.NotNull;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
@@ -61,22 +60,6 @@ public interface ControllerManagement {
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
Action addCancelActionStatus(@NotNull ActionStatusCreate create);
/**
* Sends the download progress and notifies the event publisher with a
* {@link DownloadProgressEvent}.
*
* @param statusId
* the ID of the {@link ActionStatus}
* @param requestedBytes
* requested bytes of the request
* @param shippedBytesSinceLast
* since the last report
* @param shippedBytesOverall
* for the {@link ActionStatus}
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
void downloadProgress(Long statusId, Long requestedBytes, Long shippedBytesSinceLast, Long shippedBytesOverall);
/**
* Simple addition of a new {@link ActionStatus} entry to the {@link Action}
* . No state changes.

View File

@@ -11,13 +11,13 @@ package org.eclipse.hawkbit.repository;
import java.util.List;
import java.util.Optional;
import javax.validation.ConstraintViolationException;
import javax.validation.constraints.NotNull;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;

View File

@@ -8,6 +8,8 @@
*/
package org.eclipse.hawkbit.repository.event.remote;
import org.eclipse.hawkbit.repository.model.ActionStatus;
/**
* TenantAwareEvent that contains an updated download progress for a given
* ActionStatus that was written for a download request.
@@ -17,7 +19,7 @@ public class DownloadProgressEvent extends RemoteTenantAwareEvent {
private static final long serialVersionUID = 1L;
private Long shippedBytesSinceLast;
private long shippedBytesSinceLast;
/**
* Default constructor.
@@ -31,13 +33,16 @@ public class DownloadProgressEvent extends RemoteTenantAwareEvent {
*
* @param tenant
* the tenant
* @param actionStatusId
* of the {@link ActionStatus} the download belongs to
* @param shippedBytesSinceLast
* the shippedBytesSinceLast
* @param applicationId
* the application id.
*/
public DownloadProgressEvent(final String tenant, final Long shippedBytesSinceLast, final String applicationId) {
super(shippedBytesSinceLast, tenant, applicationId);
public DownloadProgressEvent(final String tenant, final Long actionStatusId, final long shippedBytesSinceLast,
final String applicationId) {
super(actionStatusId, tenant, applicationId);
this.shippedBytesSinceLast = shippedBytesSinceLast;
}

View File

@@ -1,64 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.exception;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;
/**
* the {@link ConstraintViolationException} is thrown when an entity is tried to
* be saved which has constraint violations
*/
public class ConstraintViolationException extends AbstractServerRtException {
private static final long serialVersionUID = 1L;
private static final String MESSAGE_FORMATTER_SEPARATOR = " ";
/**
* Constructor for {@link ConstraintViolationException}
*
* @param ex
* the javax.validation.ConstraintViolationException which is
* thrown
*/
public ConstraintViolationException(final javax.validation.ConstraintViolationException ex) {
this(getExceptionMessage(ex));
}
/**
* Creates a new {@link ConstraintViolationException} with the error code
* {@link SpServerError#SP_REPO_CONSTRAINT_VIOLATION}.
*
* @param msgText
* the message text for this exception
*/
public ConstraintViolationException(final String msgText) {
super(msgText, SpServerError.SP_REPO_CONSTRAINT_VIOLATION);
}
/**
* Uses the information of
* {@link javax.validation.ConstraintViolationException} to provide a proper
* error message for {@link ConstraintViolationException}
*
* @param ex
* javax.validation.ConstraintViolationException which is thrown
* @return message String with proper error information
*/
public static String getExceptionMessage(final javax.validation.ConstraintViolationException ex) {
return ex
.getConstraintViolations().stream().map(violation -> violation.getPropertyPath()
+ MESSAGE_FORMATTER_SEPARATOR + violation.getMessage() + ".")
.collect(Collectors.joining(MESSAGE_FORMATTER_SEPARATOR));
}
}

View File

@@ -8,7 +8,7 @@
*/
package org.eclipse.hawkbit.repository.model;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
/**
* Binaries for a {@link SoftwareModule} Note: the decision which artifacts have
@@ -34,7 +34,7 @@ public interface Artifact extends TenantAwareBaseEntity {
/**
* @return SHA-1 hash of the artifact in Base16 format that identifies the
* {@link DbArtifact} in the system.
* {@link AbstractDbArtifact} in the system.
*/
String getSha1Hash();

View File

@@ -12,7 +12,8 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import javax.validation.ValidationException;
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
@@ -35,10 +36,10 @@ public final class RolloutHelper {
*/
public static void verifyRolloutGroupConditions(final RolloutGroupConditions conditions) {
if (conditions.getSuccessCondition() == null) {
throw new ConstraintViolationException("Rollout group is missing success condition");
throw new ValidationException("Rollout group is missing success condition");
}
if (conditions.getSuccessAction() == null) {
throw new ConstraintViolationException("Rollout group is missing success action");
throw new ValidationException("Rollout group is missing success action");
}
}
@@ -52,14 +53,14 @@ public final class RolloutHelper {
*/
public static RolloutGroup verifyRolloutGroupHasConditions(final RolloutGroup group) {
if (group.getTargetPercentage() < 1F || group.getTargetPercentage() > 100F) {
throw new ConstraintViolationException("Target percentage has to be between 1 and 100");
throw new ValidationException("Target percentage has to be between 1 and 100");
}
if (group.getSuccessCondition() == null) {
throw new ConstraintViolationException("Rollout group is missing success condition");
throw new ValidationException("Rollout group is missing success condition");
}
if (group.getSuccessAction() == null) {
throw new ConstraintViolationException("Rollout group is missing success action");
throw new ValidationException("Rollout group is missing success action");
}
return group;
}
@@ -74,9 +75,9 @@ public final class RolloutHelper {
*/
public static void verifyRolloutGroupParameter(final int amountGroup, final QuotaManagement quotaManagement) {
if (amountGroup <= 0) {
throw new ConstraintViolationException("the amount of groups cannot be lower than zero");
throw new ValidationException("the amount of groups cannot be lower than zero");
} else if (amountGroup > quotaManagement.getMaxRolloutGroupsPerRollout()) {
throw new ConstraintViolationException("the amount of groups cannot be greater than 500");
throw new ValidationException("the amount of groups cannot be greater than 500");
}
}
@@ -88,9 +89,9 @@ public final class RolloutHelper {
*/
public static void verifyRolloutGroupTargetPercentage(final float percentage) {
if (percentage <= 0) {
throw new ConstraintViolationException("the percentage must be greater than zero");
throw new ValidationException("the percentage must be greater than zero");
} else if (percentage > 100) {
throw new ConstraintViolationException("the percentage must not be greater than 100");
throw new ValidationException("the percentage must not be greater than 100");
}
}
@@ -240,11 +241,10 @@ public final class RolloutHelper {
*/
public static void verifyRemainingTargets(final long targetCount) {
if (targetCount > 0) {
throw new ConstraintViolationException(
"Rollout groups don't match all targets that are targeted by the rollout");
throw new ValidationException("Rollout groups don't match all targets that are targeted by the rollout");
}
if (targetCount != 0) {
throw new ConstraintViolationException("Rollout groups target count verification failed");
throw new ValidationException("Rollout groups target count verification failed");
}
}

View File

@@ -14,7 +14,7 @@ import java.util.Optional;
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
import org.eclipse.hawkbit.artifact.repository.ArtifactStoreException;
import org.eclipse.hawkbit.artifact.repository.HashNotMatchException;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException;
@@ -88,7 +88,7 @@ public class JpaArtifactManagement implements ArtifactManagement {
public Artifact createArtifact(final InputStream stream, final Long moduleId, final String filename,
final String providedMd5Sum, final String providedSha1Sum, final boolean overrideExisting,
final String contentType) {
DbArtifact result = null;
AbstractDbArtifact result = null;
final SoftwareModule softwareModule = getModuleAndThrowExceptionIfThatFails(moduleId);
@@ -184,12 +184,12 @@ public class JpaArtifactManagement implements ArtifactManagement {
}
@Override
public Optional<DbArtifact> loadArtifactBinary(final String sha1Hash) {
public Optional<AbstractDbArtifact> loadArtifactBinary(final String sha1Hash) {
return Optional.ofNullable(artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), sha1Hash));
}
private Artifact storeArtifactMetadata(final SoftwareModule softwareModule, final String providedFilename,
final DbArtifact result, final Artifact existing) {
final AbstractDbArtifact result, final Artifact existing) {
JpaArtifact artifact = (JpaArtifact) existing;
if (existing == null) {
artifact = new JpaArtifact(result.getHashes().getSha1(), providedFilename, softwareModule);

View File

@@ -26,7 +26,6 @@ import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
@@ -483,13 +482,6 @@ public class JpaControllerManagement implements ControllerManagement {
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
}
@Override
public void downloadProgress(final Long statusId, final Long requestedBytes, final Long shippedBytesSinceLast,
final Long shippedBytesOverall) {
eventPublisher.publishEvent(new DownloadProgressEvent(tenantAware.getCurrentTenant(), shippedBytesSinceLast,
applicationContext.getId()));
}
@Override
public Optional<Target> findByControllerId(final String controllerId) {
return targetRepository.findByControllerId(controllerId);

View File

@@ -19,6 +19,7 @@ import java.util.stream.StreamSupport;
import javax.persistence.EntityManager;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ValidationException;
import org.eclipse.hawkbit.repository.AbstractRolloutManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
@@ -37,7 +38,6 @@ import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
@@ -216,7 +216,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
final Long totalTargets = targetManagement.countTargetByTargetFilterQuery(rollout.getTargetFilterQuery());
if (totalTargets == 0) {
throw new ConstraintViolationException("Rollout does not match any existing targets");
throw new ValidationException("Rollout does not match any existing targets");
}
rollout.setTotalTargets(totalTargets);
return rolloutRepository.save(rollout);

View File

@@ -8,10 +8,11 @@
*/
package org.eclipse.hawkbit.repository.jpa.builder;
import javax.validation.ValidationException;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.builder.AbstractSoftwareModuleUpdateCreate;
import org.eclipse.hawkbit.repository.builder.SoftwareModuleCreate;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
@@ -36,7 +37,7 @@ public class JpaSoftwareModuleCreate extends AbstractSoftwareModuleUpdateCreate<
private SoftwareModuleType getSoftwareModuleTypeFromKeyString(final String type) {
if (type == null) {
throw new ConstraintViolationException("type cannot be null");
throw new ValidationException("type cannot be null");
}
return softwareModuleTypeManagement.findSoftwareModuleTypeByKey(type.trim())

View File

@@ -21,7 +21,7 @@ import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.hibernate.validator.constraints.NotEmpty;
@@ -71,9 +71,9 @@ public class JpaArtifact extends AbstractJpaTenantAwareBaseEntity implements Art
* Constructs artifact.
*
* @param sha1Hash
* that is the link to the {@link DbArtifact} entity.
* that is the link to the {@link AbstractDbArtifact} entity.
* @param filename
* that is used by {@link DbArtifact} store.
* that is used by {@link AbstractDbArtifact} store.
* @param softwareModule
* of this artifact
*/

View File

@@ -29,7 +29,7 @@ public class RemoteTenantAwareEventTest extends AbstractRemoteEventTest {
@Test
@Description("Verifies that the download progress reloading by remote events works")
public void reloadDownloadProgessByRemoteEvent() {
final DownloadProgressEvent downloadProgressEvent = new DownloadProgressEvent("DEFAULT", 3L, "Node");
final DownloadProgressEvent downloadProgressEvent = new DownloadProgressEvent("DEFAULT", 1L, 3L, "Node");
DownloadProgressEvent remoteEvent = (DownloadProgressEvent) createProtoStuffEvent(downloadProgressEvent);
assertThat(downloadProgressEvent).isEqualTo(remoteEvent);

View File

@@ -26,7 +26,6 @@ public class ActionTest {
@Description("Ensures that timeforced moded switch from soft to forces after defined timeframe.")
public void timeforcedHitNewHasCodeIsGenerated() throws InterruptedException {
final boolean active;
// current time + 1 seconds
final long sleepTime = 1000;
final long timeForceTimeAt = System.currentTimeMillis() + sleepTime;

View File

@@ -104,10 +104,8 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest {
final Artifact result = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(), "file1",
false);
final Artifact result11 = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(),
"file11", false);
final Artifact result12 = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(),
"file12", false);
artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(), "file11", false);
artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(), "file12", false);
final Artifact result2 = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm2.getId(),
"file2", false);
@@ -270,8 +268,7 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest {
assertThat(artifactManagement.findArtifactBySoftwareModule(PAGE, sm.getId())).isEmpty();
final Artifact result = artifactManagement.createArtifact(new RandomGeneratedInputStream(5 * 1024), sm.getId(),
"file1", false);
artifactManagement.createArtifact(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file1", false);
assertThat(artifactManagement.findArtifactBySoftwareModule(PAGE, sm.getId())).hasSize(1);
}

View File

@@ -747,7 +747,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
}
@Test
@Description("Deletes multiple targets and verfies that all related metadat is also deleted.")
@Description("Deletes multiple targets and verfies that all related metadata is also deleted.")
public void deletesTargetsAndVerifyCascadeDeletes() {
final String undeployedTargetPrefix = "undep-T";

View File

@@ -21,6 +21,8 @@ import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.validation.ValidationException;
import org.assertj.core.api.Condition;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
@@ -37,7 +39,6 @@ import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
@@ -139,7 +140,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
@Expect(type = RolloutUpdatedEvent.class, count = 1),
@Expect(type = TargetCreatedEvent.class, count = 10) })
public void entityQueriesReferringToNotExistingEntitiesThrowsException() {
final Rollout createdRollout = testdataFactory.createRollout("xxx");
testdataFactory.createRollout("xxx");
verifyThrownExceptionBy(() -> rolloutManagement.deleteRollout(NOT_EXIST_IDL), "Rollout");
@@ -1089,7 +1090,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName);
assertThatExceptionOfType(ConstraintViolationException.class)
assertThatExceptionOfType(ValidationException.class)
.isThrownBy(() -> testdataFactory.createRolloutByVariables(rolloutName, "desc", amountGroups,
"id==notExisting", distributionSet, successCondition, errorCondition))
.withMessageContaining("does not match any existing");
@@ -1336,7 +1337,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
rolloutGroups.add(generateRolloutGroup(0, percentTargetsInGroup1, null));
rolloutGroups.add(generateRolloutGroup(1, percentTargetsInGroup2, null));
assertThatExceptionOfType(ConstraintViolationException.class)
assertThatExceptionOfType(ValidationException.class)
.isThrownBy(() -> rolloutManagement.createRollout(myRollout, rolloutGroups, conditions))
.withMessageContaining("groups don't match");
@@ -1357,7 +1358,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
generateRolloutGroup(0, percentTargetsInGroup1, null),
generateRolloutGroup(1, percentTargetsInGroup2, null));
assertThatExceptionOfType(ConstraintViolationException.class)
assertThatExceptionOfType(ValidationException.class)
.isThrownBy(() -> rolloutManagement.createRollout(myRollout, rolloutGroups, conditions))
.withMessageContaining("percentage has to be between 1 and 100");
@@ -1373,7 +1374,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults().build();
final RolloutCreate myRollout = generateTargetsAndRollout(rolloutName, amountTargetsForRollout);
assertThatExceptionOfType(ConstraintViolationException.class)
assertThatExceptionOfType(ValidationException.class)
.isThrownBy(() -> rolloutManagement.createRollout(myRollout, illegalGroupAmount, conditions))
.withMessageContaining("not be greater than " + quotaManagement.getMaxRolloutGroupsPerRollout());

View File

@@ -252,8 +252,8 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
@Description("Counts all software modules in the repsitory that are not marked as deleted.")
public void countSoftwareModulesAll() {
// found in test
final SoftwareModule one = testdataFactory.createSoftwareModuleOs("one");
final SoftwareModule two = testdataFactory.createSoftwareModuleOs("two");
testdataFactory.createSoftwareModuleOs("one");
testdataFactory.createSoftwareModuleOs("two");
final SoftwareModule deleted = testdataFactory.createSoftwareModuleOs("deleted");
// ignored
softwareModuleManagement.deleteSoftwareModule(deleted.getId());

View File

@@ -484,8 +484,6 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest {
*/
private void checkTargetHasTags(final boolean strict, final Iterable<Target> targets, final TargetTag... tags) {
_target: for (final Target tl : targets) {
final Target t = targetManagement.findTargetByControllerID(tl.getControllerId()).get();
for (final Tag tt : tagManagement.findAllTargetTags(PAGE, tl.getControllerId())) {
for (final Tag tag : tags) {
if (tag.getName().equals(tt.getName())) {
@@ -502,7 +500,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest {
private void checkTargetHasNotTags(final Iterable<Target> targets, final TargetTag... tags) {
for (final Target tl : targets) {
final Target t = targetManagement.findTargetByControllerID(tl.getControllerId()).get();
targetManagement.findTargetByControllerID(tl.getControllerId()).get();
for (final Tag tag : tags) {
for (final Tag tt : tagManagement.findAllTargetTags(PAGE, tl.getControllerId())) {

View File

@@ -315,8 +315,8 @@ public class RSQLUtilityTest {
.thenReturn(mock(Predicate.class));
// test
final Predicate result = RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup())
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup()).toPredicate(baseSoftwareModuleRootMock,
criteriaQueryMock, criteriaBuilderMock);
// verification
verify(macroResolver).lookup(overdueProp);

View File

@@ -14,8 +14,6 @@ import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import java.time.Instant;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.TimestampCalculator;
@@ -48,8 +46,6 @@ public class VirtualPropertyResolverTest {
private StrSubstitutor substitutor;
private Long nowTestTime;
private static final TenantConfigurationValue<String> TEST_POLLING_TIME_INTERVAL = TenantConfigurationValue
.<String> builder().value("00:05:00").build();
private static final TenantConfigurationValue<String> TEST_POLLING_OVERDUE_TIME_INTERVAL = TenantConfigurationValue
@@ -57,7 +53,6 @@ public class VirtualPropertyResolverTest {
@Before
public void before() {
nowTestTime = Instant.now().toEpochMilli();
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class))
.thenReturn(TEST_POLLING_TIME_INTERVAL);
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class))

View File

@@ -21,22 +21,34 @@
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
@@ -58,6 +70,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-repository-jpa</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ru.yandex.qatools.allure</groupId>
<artifactId>allure-junit-adaptor</artifactId>

View File

@@ -12,9 +12,6 @@ import org.eclipse.hawkbit.rest.exception.SortParameterUnsupportedDirectionExcep
/**
* A definition of possible sorting direction.
*
*
*
*/
public enum SortDirection {
/**

View File

@@ -6,7 +6,7 @@
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.exception;
package org.eclipse.hawkbit.rest.exception;
import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;

View File

@@ -11,14 +11,15 @@ package org.eclipse.hawkbit.rest.exception;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.repository.exception.MultiPartFileUploadException;
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,6 +42,8 @@ public class ResponseExceptionHandler {
private static final Map<SpServerError, HttpStatus> ERROR_TO_HTTP_STATUS = new EnumMap<>(SpServerError.class);
private static final HttpStatus DEFAULT_RESPONSE_STATUS = HttpStatus.INTERNAL_SERVER_ERROR;
private static final String MESSAGE_FORMATTER_SEPARATOR = " ";
static {
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_NOT_EXISTS, HttpStatus.NOT_FOUND);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_ALRREADY_EXISTS, HttpStatus.CONFLICT);
@@ -138,12 +141,40 @@ public class ResponseExceptionHandler {
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ExceptionInfo> handleConstraintViolationException(final HttpServletRequest request,
final Exception ex) {
final ConstraintViolationException ex) {
logRequest(request, ex);
final ExceptionInfo response = createExceptionInfo(
new org.eclipse.hawkbit.repository.exception.ConstraintViolationException(
(ConstraintViolationException) ex));
final ExceptionInfo response = new ExceptionInfo();
response.setMessage(ex.getConstraintViolations().stream().map(
violation -> violation.getPropertyPath() + MESSAGE_FORMATTER_SEPARATOR + violation.getMessage() + ".")
.collect(Collectors.joining(MESSAGE_FORMATTER_SEPARATOR)));
response.setExceptionClass(ex.getClass().getName());
response.setErrorCode(SpServerError.SP_REPO_CONSTRAINT_VIOLATION.getKey());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
/**
* Method for handling exception of type ValidationException which is thrown
* in case the request is rejected due to invalid requests. Called by the
* Spring-Framework for exception handling.
*
* @param request
* the Http request
* @param ex
* the exception which occurred
* @return the entity to be responded containing the exception information
* as entity.
*/
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ExceptionInfo> handleValidationException(final HttpServletRequest request,
final ValidationException ex) {
logRequest(request, ex);
final ExceptionInfo response = new ExceptionInfo();
response.setMessage(ex.getMessage());
response.setExceptionClass(ex.getClass().getName());
response.setErrorCode(SpServerError.SP_REPO_CONSTRAINT_VIOLATION.getKey());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

View File

@@ -1,112 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.rest.util;
/**
* Byte range for resume download operations.
*
*
*
*
*
*/
public class ByteRange {
public static final String MULTIPART_BOUNDARY = "THIS_STRING_SEPARATES_MULTIPART";
private final long start;
private final long end;
private final long length;
private final long total;
/**
* Construct a byte range.
*
* @param start
* Start of the byte range.
* @param end
* End of the byte range.
* @param total
* Total length of the byte source.
*/
public ByteRange(final long start, final long end, final long total) {
this.start = start;
this.end = end;
length = end - start + 1;
this.total = total;
}
/**
* @return the start
*/
public long getStart() {
return start;
}
/**
* @return the end
*/
public long getEnd() {
return end;
}
/**
* @return the length
*/
public long getLength() {
return length;
}
/**
* @return the total
*/
public long getTotal() {
return total;
}
@Override
// NOSONAR - as this is generated
@SuppressWarnings("squid:S864")
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (end ^ end >>> 32);
result = prime * result + (int) (length ^ length >>> 32);
result = prime * result + (int) (start ^ start >>> 32);
result = prime * result + (int) (total ^ total >>> 32);
return result;
}
@Override
public boolean equals(final Object obj) { // NOSONAR - as this is generated
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ByteRange other = (ByteRange) obj;
if (end != other.end) {
return false;
}
if (length != other.length) {
return false;
}
if (start != other.start) {
return false;
}
if (total != other.total) {
return false;
}
return true;
}
}

View File

@@ -14,7 +14,7 @@ import org.eclipse.hawkbit.exception.SpServerError;
/**
* Thrown if artifact content streaming to client failed.
*/
public final class FileSteamingFailedException extends AbstractServerRtException {
public final class FileStreamingFailedException extends AbstractServerRtException {
private static final long serialVersionUID = 1L;
@@ -22,7 +22,7 @@ public final class FileSteamingFailedException extends AbstractServerRtException
* Creates a new FileUploadFailedException with
* {@link SpServerError#SP_REST_BODY_NOT_READABLE} error.
*/
public FileSteamingFailedException() {
public FileStreamingFailedException() {
super(SpServerError.SP_ARTIFACT_LOAD_FAILED);
}
@@ -32,7 +32,7 @@ public final class FileSteamingFailedException extends AbstractServerRtException
* @param cause
* for the exception
*/
public FileSteamingFailedException(final Throwable cause) {
public FileStreamingFailedException(final Throwable cause) {
super(SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
}
@@ -42,7 +42,19 @@ public final class FileSteamingFailedException extends AbstractServerRtException
* @param message
* of the error
*/
public FileSteamingFailedException(final String message) {
public FileStreamingFailedException(final String message) {
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED);
}
/**
* Constructor with error string and cause.
*
* @param message
* of the error
* @param cause
* for the exception
*/
public FileStreamingFailedException(final String message, final Throwable cause) {
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.rest.util;
/**
* Listener for progress on artifact file streaming.
*
*/
@FunctionalInterface
public interface FileStreamingProgressListener {
/**
* Called multiple times during streaming.
*
* @param requestedBytes
* requested bytes of the request
* @param shippedBytesSinceLast
* since the last report
* @param shippedBytesOverall
* during the request
*/
void progress(long requestedBytes, long shippedBytesSinceLast, long shippedBytesOverall);
}

View File

@@ -0,0 +1,445 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.rest.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import com.google.common.math.DoubleMath;
/**
* Utility class for artifact file streaming.
*/
public final class FileStreamingUtil {
private static final Logger LOG = LoggerFactory.getLogger(FileStreamingUtil.class);
/**
* File suffix for MDH hash download (see Linux md5sum).
*/
public static final String ARTIFACT_MD5_DWNL_SUFFIX = ".MD5SUM";
private static final int BUFFER_SIZE = 0x2000; // 8k
private FileStreamingUtil() {
}
/**
* Write a md5 file response.
*
* @param response
* the response
* @param md5Hash
* of the artifact
* @param filename
* as provided by the client
* @return the response
* @throws IOException
* cannot write output stream
*/
public static ResponseEntity<Void> writeMD5FileResponse(final HttpServletResponse response, final String md5Hash,
final String filename) throws IOException {
if (md5Hash == null) {
return ResponseEntity.notFound().build();
}
final StringBuilder builder = new StringBuilder();
builder.append(md5Hash);
builder.append(" ");
builder.append(filename);
final byte[] content = builder.toString().getBytes(StandardCharsets.US_ASCII);
final StringBuilder header = new StringBuilder().append("attachment;filename=").append(filename)
.append(ARTIFACT_MD5_DWNL_SUFFIX);
response.setContentLength(content.length);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header.toString());
response.getOutputStream().write(content);
return ResponseEntity.ok().build();
}
/**
* <p>
* Write response with target relation and publishes events concerning the
* download progress based on given update action status.
* </p>
*
* <p>
* The request supports RFC7233 range requests.
* </p>
*
* @param artifact
* the artifact
* @param filename
* to be written to the client response
* @param lastModified
* unix timestamp of the artifact
* @param response
* to be sent back to the requesting client
* @param request
* from the client
* @param progressListener
* to write progress updates to
*
* @return http response
*
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org
* /html/rfc7233</a>
*
* @throws FileStreamingFailedException
* if streaming fails
*/
public static ResponseEntity<InputStream> writeFileResponse(final AbstractDbArtifact artifact,
final String filename, final Long lastModified, final HttpServletResponse response,
final HttpServletRequest request, final FileStreamingProgressListener progressListener) {
ResponseEntity<InputStream> result;
final String etag = artifact.getHashes().getSha1();
final long length = artifact.getSize();
response.reset();
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
response.setHeader(HttpHeaders.ETAG, etag);
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
if (lastModified != null) {
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified);
}
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setBufferSize(BUFFER_SIZE);
final ByteRange full = new ByteRange(0, length - 1, length);
final List<ByteRange> ranges = new ArrayList<>();
// Validate and process Range and If-Range headers.
final String range = request.getHeader("Range");
if (lastModified != null && range != null) {
LOG.debug("range header for filename ({}) is: {}", filename, range);
// Range header matches"bytes=n-n,n-n,n-n..."
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
LOG.debug("range header for filename ({}) is not satisfiable: ", filename);
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
}
// RFC: if the representation is unchanged, send me the part(s) that
// I am requesting in
// Range; otherwise, send me the entire representation.
checkForShortcut(request, etag, lastModified, full, ranges);
// it seems there are valid ranges
result = extractRange(response, length, ranges, range);
// return if range extraction turned out to be invalid
if (result != null) {
return result;
}
}
// full request - no range
if (ranges.isEmpty() || ranges.get(0).equals(full)) {
LOG.debug("filename ({}) results into a full request: ", filename);
result = handleFullFileRequest(artifact, filename, response, progressListener, full);
}
// standard range request
else if (ranges.size() == 1) {
LOG.debug("filename ({}) results into a standard range request: ", filename);
result = handleStandardRangeRequest(artifact, filename, response, progressListener, ranges);
}
// multipart range request
else {
LOG.debug("filename ({}) results into a multipart range request: ", filename);
result = handleMultipartRangeRequest(artifact, filename, response, progressListener, ranges);
}
return result;
}
private static ResponseEntity<InputStream> handleFullFileRequest(final AbstractDbArtifact artifact,
final String filename, final HttpServletResponse response,
final FileStreamingProgressListener progressListener, final ByteRange full) {
final ByteRange r = full;
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
response.setContentLengthLong(r.getLength());
try (InputStream from = artifact.getFileInputStream()) {
final ServletOutputStream to = response.getOutputStream();
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
} catch (final IOException e) {
throw new FileStreamingFailedException("fullfileRequest " + filename, e);
}
return ResponseEntity.ok().build();
}
private static ResponseEntity<InputStream> extractRange(final HttpServletResponse response, final long length,
final List<ByteRange> ranges, final String range) {
if (ranges.isEmpty()) {
for (final String part : range.substring(6).split(",")) {
long start = sublong(part, 0, part.indexOf('-'));
long end = sublong(part, part.indexOf('-') + 1, part.length());
if (start == -1) {
start = length - end;
end = length - 1;
} else if (end == -1 || end > length - 1) {
end = length - 1;
}
// Check if Range is syntactically valid. If not, then return
// 416.
if (start > end) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
}
// Add range.
ranges.add(new ByteRange(start, end, length));
}
}
return null;
}
private static long sublong(final String value, final int beginIndex, final int endIndex) {
final String substring = value.substring(beginIndex, endIndex);
return substring.length() > 0 ? Long.parseLong(substring) : -1;
}
private static void checkForShortcut(final HttpServletRequest request, final String etag, final long lastModified,
final ByteRange full, final List<ByteRange> ranges) {
final String ifRange = request.getHeader(HttpHeaders.IF_RANGE);
if (ifRange != null && !ifRange.equals(etag)) {
try {
final long ifRangeTime = request.getDateHeader(HttpHeaders.IF_RANGE);
if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
ranges.add(full);
}
} catch (final IllegalArgumentException ignore) {
LOG.info("Invalid if-range header field", ignore);
ranges.add(full);
}
}
}
private static ResponseEntity<InputStream> handleMultipartRangeRequest(final AbstractDbArtifact artifact,
final String filename, final HttpServletResponse response,
final FileStreamingProgressListener progressListener, final List<ByteRange> ranges) {
response.setContentType("multipart/byteranges; boundary=" + ByteRange.MULTIPART_BOUNDARY);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
try {
final ServletOutputStream to = response.getOutputStream();
for (final ByteRange r : ranges) {
try (InputStream from = artifact.getFileInputStream()) {
// Add multipart boundary and header fields for every range.
to.println();
to.println("--" + ByteRange.MULTIPART_BOUNDARY);
to.println(HttpHeaders.CONTENT_RANGE + ": bytes " + r.getStart() + "-" + r.getEnd() + "/"
+ r.getTotal());
// Copy single part range of multi part range.
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
}
}
// End with final multipart boundary.
to.println();
to.print("--" + ByteRange.MULTIPART_BOUNDARY + "--");
} catch (final IOException e) {
throw new FileStreamingFailedException("multipartRangeRequest " + filename, e);
}
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
}
private static ResponseEntity<InputStream> handleStandardRangeRequest(final AbstractDbArtifact artifact,
final String filename, final HttpServletResponse response,
final FileStreamingProgressListener progressListener, final List<ByteRange> ranges) {
final ByteRange r = ranges.get(0);
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
response.setContentLengthLong(r.getLength());
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
try (InputStream from = artifact.getFileInputStream()) {
final ServletOutputStream to = response.getOutputStream();
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
} catch (final IOException e) {
LOG.error("standardRangeRequest of file ({}) failed!", filename, e);
throw new FileStreamingFailedException(filename);
}
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
}
private static long copyStreams(final InputStream from, final OutputStream to,
final FileStreamingProgressListener progressListener, final long start, final long length,
final String filename) throws IOException {
final long startMillis = System.currentTimeMillis();
LOG.trace("Start of copy-streams of file {} from {} to {}", filename, start, length);
Preconditions.checkNotNull(from);
Preconditions.checkNotNull(to);
final byte[] buf = new byte[BUFFER_SIZE];
long total = 0;
int progressPercent = 1;
ByteStreams.skipFully(from, start);
long toRead = length;
boolean toContinue = true;
long shippedSinceLastEvent = 0;
while (toContinue) {
final int r = from.read(buf);
if (r == -1) {
break;
}
toRead -= r;
if (toRead > 0) {
to.write(buf, 0, r);
total += r;
shippedSinceLastEvent += r;
} else {
to.write(buf, 0, (int) toRead + r);
total += toRead + r;
shippedSinceLastEvent += toRead + r;
toContinue = false;
}
if (progressListener != null) {
final int newPercent = DoubleMath.roundToInt(total * 100.0 / length, RoundingMode.DOWN);
// every 10 percent an event
if (newPercent == 100 || newPercent > progressPercent + 10) {
progressPercent = newPercent;
progressListener.progress(length, shippedSinceLastEvent, total);
shippedSinceLastEvent = 0;
}
}
}
final long totalTime = System.currentTimeMillis() - startMillis;
if (total < length) {
throw new FileStreamingFailedException(filename + ": " + (length - total)
+ " bytes could not be written to client, total time on write: !" + totalTime + " ms");
}
LOG.trace("Finished copy-stream of file {} with length {} in {} ms", filename, length, totalTime);
return total;
}
private static final class ByteRange {
private static final String MULTIPART_BOUNDARY = "THIS_STRING_SEPARATES_MULTIPART";
private final long start;
private final long end;
private final long length;
private final long total;
private ByteRange(final long start, final long end, final long total) {
this.start = start;
this.end = end;
length = end - start + 1;
this.total = total;
}
private long getStart() {
return start;
}
private long getEnd() {
return end;
}
private long getLength() {
return length;
}
private long getTotal() {
return total;
}
@Override
// Generated code
@SuppressWarnings("squid:S864")
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (end ^ (end >>> 32));
result = prime * result + (int) (length ^ (length >>> 32));
result = prime * result + (int) (start ^ (start >>> 32));
result = prime * result + (int) (total ^ (total >>> 32));
return result;
}
@Override
// Generated code
@SuppressWarnings("squid:S1126")
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ByteRange other = (ByteRange) obj;
if (end != other.end) {
return false;
}
if (length != other.length) {
return false;
}
if (start != other.start) {
return false;
}
if (total != other.total) {
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.rest.util;
import java.util.Arrays;
/**
* Utility class for the Rest Source API.
*/
public final class HttpUtil {
private HttpUtil() {
}
/**
* Checks given CSV string for defined match value or wildcard.
*
* @param matchHeader
* to search through
* @param toMatch
* to search for
*
* @return <code>true</code> if string matches.
*/
public static boolean matchesHttpHeader(final String matchHeader, final String toMatch) {
final String[] matchValues = matchHeader.split("\\s*,\\s*");
Arrays.sort(matchValues);
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
}
}

View File

@@ -1,352 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.rest.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import com.google.common.base.Preconditions;
import com.google.common.math.DoubleMath;
import com.google.common.net.HttpHeaders;
/**
* Utility class for the Rest Source API.
*/
public final class RestResourceConversionHelper {
private static final Logger LOG = LoggerFactory.getLogger(RestResourceConversionHelper.class);
private static final int BUFFER_SIZE = 4096;
private RestResourceConversionHelper() {
}
/**
* Write response without target relation.
*
* @param artifact
* the artifact
* @param servletResponse
* to be sent back to the requesting client
* @param request
* from the client
* @param file
* to be write to the client response
*
* @return http code
*/
public static ResponseEntity<InputStream> writeFileResponse(final Artifact artifact,
final HttpServletResponse servletResponse, final HttpServletRequest request, final DbArtifact file) {
return writeFileResponse(artifact, servletResponse, request, file, null, null);
}
/**
* <p>
* Write response with target relation and publishes events concerning the
* download progress based on given update action status.
* </p>
*
* <p>
* The request supports RFC7233 range requests.
* </p>
*
* @param artifact
* the artifact
* @param response
* to be sent back to the requesting client
* @param request
* from the client
* @param file
* to be write to the client response
* @param controllerManagement
* to write progress updates to
* @param statusId
* of the {@link ActionStatus}
*
* @return http code
*
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org
* /html/rfc7233</a>
*/
public static ResponseEntity<InputStream> writeFileResponse(final Artifact artifact,
final HttpServletResponse response, final HttpServletRequest request, final DbArtifact file,
final ControllerManagement controllerManagement, final Long statusId) {
ResponseEntity<InputStream> result;
final String etag = artifact.getSha1Hash();
final Long lastModified = artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt()
: artifact.getCreatedAt();
final long length = file.getSize();
response.reset();
response.setBufferSize(BUFFER_SIZE);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + artifact.getFilename());
response.setHeader(HttpHeaders.ETAG, etag);
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
final ByteRange full = new ByteRange(0, length - 1, length);
final List<ByteRange> ranges = new ArrayList<>();
// Validate and process Range and If-Range headers.
final String range = request.getHeader("Range");
if (range != null) {
LOG.debug("range header for filename ({}) is: {}", artifact.getFilename(), range);
// Range header matches"bytes=n-n,n-n,n-n..."
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
LOG.debug("range header for filename ({}) is not satisfiable: ", artifact.getFilename());
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
}
// RFC: if the representation is unchanged, send me the part(s) that
// I am requesting in
// Range; otherwise, send me the entire representation.
checkForShortcut(request, etag, lastModified, full, ranges);
// it seems there are valid ranges
result = extractRange(response, length, ranges, range);
// return if range extraction turned out to be invalid
if (result != null) {
return result;
}
}
// full request - no range
if (ranges.isEmpty() || ranges.get(0).equals(full)) {
LOG.debug("filename ({}) results into a full request: ", artifact.getFilename());
handleFullFileRequest(artifact, response, file, controllerManagement, statusId, full);
result = ResponseEntity.ok().build();
}
// standard range request
else if (ranges.size() == 1) {
LOG.debug("filename ({}) results into a standard range request: ", artifact.getFilename());
handleStandardRangeRequest(artifact, response, file, controllerManagement, statusId, ranges);
result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT);
}
// multipart range request
else {
LOG.debug("filename ({}) results into a multipart range request: ", artifact.getFilename());
handleMultipartRangeRequest(artifact, response, file, controllerManagement, statusId, ranges);
result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT);
}
return result;
}
private static void handleFullFileRequest(final Artifact artifact, final HttpServletResponse response,
final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId,
final ByteRange full) {
final ByteRange r = full;
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength()));
try (InputStream inputStream = file.getFileInputStream()) {
copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(),
r.getLength());
} catch (final IOException e) {
LOG.error("fullfileRequest of file ({}) failed!", artifact.getFilename(), e);
throw new FileSteamingFailedException(artifact.getFilename());
}
}
private static ResponseEntity<InputStream> extractRange(final HttpServletResponse response, final long length,
final List<ByteRange> ranges, final String range) {
if (ranges.isEmpty()) {
for (final String part : range.substring(6).split(",")) {
long start = sublong(part, 0, part.indexOf('-'));
long end = sublong(part, part.indexOf('-') + 1, part.length());
if (start == -1) {
start = length - end;
end = length - 1;
} else if (end == -1 || end > length - 1) {
end = length - 1;
}
// Check if Range is syntactically valid. If not, then return
// 416.
if (start > end) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
}
// Add range.
ranges.add(new ByteRange(start, end, length));
}
}
return null;
}
private static long sublong(final String value, final int beginIndex, final int endIndex) {
final String substring = value.substring(beginIndex, endIndex);
return substring.length() > 0 ? Long.parseLong(substring) : -1;
}
private static void checkForShortcut(final HttpServletRequest request, final String etag, final long lastModified,
final ByteRange full, final List<ByteRange> ranges) {
final String ifRange = request.getHeader(HttpHeaders.IF_RANGE);
if (ifRange != null && !ifRange.equals(etag)) {
try {
final long ifRangeTime = request.getDateHeader(HttpHeaders.IF_RANGE);
if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
ranges.add(full);
}
} catch (final IllegalArgumentException ignore) {
LOG.info("Invalid if-range header field", ignore);
ranges.add(full);
}
}
}
private static void handleMultipartRangeRequest(final Artifact artifact, final HttpServletResponse response,
final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId,
final List<ByteRange> ranges) {
response.setContentType("multipart/byteranges; boundary=" + ByteRange.MULTIPART_BOUNDARY);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
for (final ByteRange r : ranges) {
try (InputStream inputStream = file.getFileInputStream()) {
// Add multipart boundary and header fields for every range.
response.getOutputStream().println();
response.getOutputStream().println("--" + ByteRange.MULTIPART_BOUNDARY);
response.getOutputStream()
.println("Content-Range: bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
// Copy single part range of multi part range.
copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(),
r.getLength());
} catch (final IOException e) {
throwFileStreamingFailedException(artifact, e);
}
}
try {
// End with final multipart boundary.
response.getOutputStream().println();
response.getOutputStream().print("--" + ByteRange.MULTIPART_BOUNDARY + "--");
} catch (final IOException e) {
throwFileStreamingFailedException(artifact, e);
}
}
private static void throwFileStreamingFailedException(final Artifact artifact, final IOException e) {
LOG.error("multipartRangeRequest of file ({}) failed!", artifact.getFilename(), e);
throw new FileSteamingFailedException(artifact.getFilename());
}
private static void handleStandardRangeRequest(final Artifact artifact, final HttpServletResponse response,
final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId,
final List<ByteRange> ranges) {
final ByteRange r = ranges.get(0);
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength()));
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
try (InputStream inputStream = file.getFileInputStream()) {
copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(),
r.getLength());
} catch (final IOException e) {
LOG.error("standardRangeRequest of file ({}) failed!", artifact.getFilename(), e);
throw new FileSteamingFailedException(artifact.getFilename());
}
}
private static long copyStreams(final InputStream from, final OutputStream to,
final ControllerManagement controllerManagement, final Long statusId, final long start, final long length)
throws IOException {
Preconditions.checkNotNull(from);
Preconditions.checkNotNull(to);
final byte[] buf = new byte[BUFFER_SIZE];
long total = 0;
int progressPercent = 1;
// skipp until start is reached
long skipped = 0;
do {
skipped += from.skip(start);
} while (skipped < start);
long toRead = length;
boolean toContinue = true;
long shippedSinceLastEvent = 0;
while (toContinue) {
final int r = from.read(buf);
if (r == -1) {
break;
}
toRead -= r;
if (toRead > 0) {
to.write(buf, 0, r);
total += r;
shippedSinceLastEvent += r;
} else {
to.write(buf, 0, (int) toRead + r);
total += toRead + r;
shippedSinceLastEvent += toRead + r;
toContinue = false;
}
if (controllerManagement != null) {
final int newPercent = DoubleMath.roundToInt(total * 100.0 / length, RoundingMode.DOWN);
// every 10 percent an event
if (newPercent == 100 || newPercent > progressPercent + 10) {
progressPercent = newPercent;
controllerManagement.downloadProgress(statusId, length, shippedSinceLastEvent, total);
shippedSinceLastEvent = 0;
}
}
}
return total;
}
/**
* Checks given CSV string for defined match value or * wildcard.
*
* @param matchHeader
* to search through
* @param toMatch
* to search for
*
* @return <code>true</code> if string matches.
*/
public static boolean matchesHttpHeader(final String matchHeader, final String toMatch) {
final String[] matchValues = matchHeader.split("\\s*,\\s*");
Arrays.sort(matchValues);
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
}
}

View File

@@ -41,7 +41,7 @@ public class StreamAwareErrorController extends BasicErrorController {
}
@RequestMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
ResponseEntity<Void> errorStream(final HttpServletRequest request, final HttpServletResponse response) {
public ResponseEntity<Void> errorStream(final HttpServletRequest request, final HttpServletResponse response) {
final HttpStatus status = getStatus(request);
return new ResponseEntity<>(status);
}