diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5885708b27..96b1019306 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,6 +47,17 @@ jobs: run: | docker run -d --rm -p 43030:3030 atomgraph/fuseki --mem /test + - name: Make directory for solr config + run: mkdir -p /tmp/solr-config + + # Need to copy the config is outside of the source path, otherwise it produces permission conflicts + - name: Copy Solr Config into container + run: sudo cp -r ${{ github.workspace }}/etc/solr-config/* /tmp/solr-config/ + + - name: Run solr container as command to trigger core creation + run: | + docker run -v /tmp/solr-config:/solr_config -d --rm -p 48983:8983 solr:9 solr-precreate access /solr_config/access + - name: Checkout submodules run: git submodule update --init --recursive diff --git a/docker-compose.yml b/docker-compose.yml index ee16eac060..c54da4f291 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,4 +21,14 @@ services: image: atomgraph/fuseki ports: - "43030:3030" - command: --mem /test \ No newline at end of file + command: --mem /test + solr: + image: solr:9 + ports: + - "48983:8983" + volumes: + - ./etc/solr-config:/solr_config/config + command: + - solr-precreate + - access + - /solr_config/config \ No newline at end of file diff --git a/etc/solr-config/access/conf/solrconfig.xml b/etc/solr-config/access/conf/solrconfig.xml index aa9d74cd89..bfc6a33a2c 100755 --- a/etc/solr-config/access/conf/solrconfig.xml +++ b/etc/solr-config/access/conf/solrconfig.xml @@ -18,7 +18,7 @@ - 8.8.1 + 9.8 - + @@ -202,7 +202,7 @@ 'simple' is the default More details on the nuances of each LockFactory... - http://wiki.apache.org/lucene-java/AvailableLockFactories + https://cwiki.apache.org/confluence/display/lucene/AvailableLockFactories --> ${solr.lock.type:native} @@ -247,24 +247,8 @@ - - - - - - - + + @@ -293,7 +277,7 @@ Instead of enabling autoCommit, consider using "commitWithin" when adding documents. - http://wiki.apache.org/solr/UpdateXmlMessages + https://solr.apache.org/guide/solr/latest/indexing-guide/indexing-with-update-handlers.html maxDocs - Maximum number of documents to add since the last commit before automatically triggering a new commit. @@ -320,7 +304,7 @@ --> - ${solr.autoSoftCommit.maxTime:-1} + ${solr.autoSoftCommit.maxTime:3000} ${solr.max.booleanClauses:1024} @@ -401,13 +373,12 @@ unordered sets of *all* documents that match a query. When a new searcher is opened, its caches may be prepopulated or "autowarmed" using data from caches in the old searcher. - autowarmCount is the number of items to prepopulate. For - LRUCache, the autowarmed items will be the most recently + autowarmCount is the number of items to prepopulate. For + CaffeineCache, the autowarmed items will be the most recently accessed items. Parameters: - class - the SolrCache implementation LRUCache or - (LRUCache or FastLRUCache) + class - the SolrCache implementation (CaffeineCache by default) size - the maximum number of entries in the cache initialSize - the initial capacity (number of entries) of the cache. (see java.util.HashMap) @@ -425,7 +396,7 @@ Caches results of searches - ordered lists of document ids (DocList) based on a query, a sort, and the range of documents requested. - Additional supported parameter by LRUCache: + Additional supported parameter by CaffeineCache: maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed to occupy --> @@ -445,6 +416,7 @@ + /> --> 200 - + For most situations, this will not be useful unless you + frequently get the same search repeatedly with different sort + options, and none of them ever use "score" + --> @@ -562,19 +535,19 @@ --> - + + resourceType:Collection + - @@ -589,65 +562,6 @@ - - - - - - - - - - - - - - - @@ -746,90 +652,26 @@ - + + - explicit 10 - - - - - - - - - @@ -841,106 +683,64 @@ - + + _text_ - - - + - text_general + - + - - - default - _text_ - solr.DirectSolrSpellChecker - - internal - - 0.5 - - 2 - - 1 - - 5 - - 4 - - 0.01 - - + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - default - on - true - 10 - 5 - 5 - true - true - 10 - 5 - - - spellcheck - - - - - - - - - - true - false - - - terms - - + + + + + + + + + + + + + + + + + + + @@ -1103,21 +885,90 @@ - - http://wiki.apache.org/solr/UpdateRequestProcessor + + Field type guessing update request processors that will + attempt to parse string-typed field values as Booleans, Longs, + Doubles, or Dates, and then add schema fields with the guessed + field types Text content will be indexed as "text_general" as + well as a copy to a plain string version in *_str. + See the updateRequestProcessorChain defined later for the order they are executed in. - + These require that the schema is both managed and mutable, by + declaring schemaFactory as ManagedIndexSchemaFactory, with + mutable specified as true. + + See https://solr.apache.org/guide/solr/latest/indexing-guide/schemaless-mode.html for further explanation. + + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -1 + + + + + - - - text/plain; charset=UTF-8 - + + + + + + + + + + + %maxLen{%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p (%t) [%notEmpty{c:%X{collection}}%notEmpty{ s:%X{shard}}%notEmpty{ r:%X{replica}}%notEmpty{ x:%X{core}}%notEmpty{ t:%X{trace_id}}] %c{1.} %m%notEmpty{ =>%ex{short}}}{10240}%n + + + + + + + + %maxLen{%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p (%t) [%notEmpty{c:%X{collection}}%notEmpty{ s:%X{shard}}%notEmpty{ r:%X{replica}}%notEmpty{ x:%X{core}}%notEmpty{ t:%X{trace_id}}] %c{1.} %m%notEmpty{ =>%ex{short}}}{10240}%n + + + + + + + + + + + + + %maxLen{%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p (%t) [%notEmpty{c:%X{collection}}%notEmpty{ s:%X{shard}}%notEmpty{ r:%X{replica}}%notEmpty{ x:%X{core}}%notEmpty{ t:%X{trace_id}}] %c{1.} %m%notEmpty{ =>%ex{short}}}{10240}%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fcrepo-utils/src/main/java/edu/unc/lib/boxc/fcrepo/exceptions/RangeNotSatisfiableException.java b/fcrepo-utils/src/main/java/edu/unc/lib/boxc/fcrepo/exceptions/RangeNotSatisfiableException.java new file mode 100644 index 0000000000..7fb5503369 --- /dev/null +++ b/fcrepo-utils/src/main/java/edu/unc/lib/boxc/fcrepo/exceptions/RangeNotSatisfiableException.java @@ -0,0 +1,24 @@ +package edu.unc.lib.boxc.fcrepo.exceptions; + +import edu.unc.lib.boxc.model.api.exceptions.FedoraException; + +/** + * Error indicates that a HTTP range request could not be satisfied + * + * @author bbpennel + */ +public class RangeNotSatisfiableException extends FedoraException { + private static final long serialVersionUID = 1L; + + public RangeNotSatisfiableException(String message) { + super(message); + } + + public RangeNotSatisfiableException(Throwable cause) { + super(cause); + } + + public RangeNotSatisfiableException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/fcrepo-utils/src/main/java/edu/unc/lib/boxc/fcrepo/utils/ClientFaultResolver.java b/fcrepo-utils/src/main/java/edu/unc/lib/boxc/fcrepo/utils/ClientFaultResolver.java index 2fe48ab9c0..0775b5f19c 100644 --- a/fcrepo-utils/src/main/java/edu/unc/lib/boxc/fcrepo/utils/ClientFaultResolver.java +++ b/fcrepo-utils/src/main/java/edu/unc/lib/boxc/fcrepo/utils/ClientFaultResolver.java @@ -1,5 +1,6 @@ package edu.unc.lib.boxc.fcrepo.utils; +import edu.unc.lib.boxc.fcrepo.exceptions.RangeNotSatisfiableException; import org.apache.http.HttpStatus; import org.fcrepo.client.FcrepoOperationFailedException; @@ -31,9 +32,11 @@ public static FedoraException resolve(Exception ex) { case HttpStatus.SC_FORBIDDEN: return new AuthorizationException(ex); case HttpStatus.SC_NOT_FOUND: - return new NotFoundException(ex); + return new NotFoundException(ex); case HttpStatus.SC_CONFLICT: - return new ConflictException(ex); + return new ConflictException(ex); + case HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE: + return new RangeNotSatisfiableException(ex); } } return new FedoraException(ex); diff --git a/fcrepo-utils/src/test/java/edu/unc/lib/boxc/fcrepo/exceptions/RangeNotSatisfiableExceptionTest.java b/fcrepo-utils/src/test/java/edu/unc/lib/boxc/fcrepo/exceptions/RangeNotSatisfiableExceptionTest.java new file mode 100644 index 0000000000..4d7ca6604b --- /dev/null +++ b/fcrepo-utils/src/test/java/edu/unc/lib/boxc/fcrepo/exceptions/RangeNotSatisfiableExceptionTest.java @@ -0,0 +1,33 @@ +package edu.unc.lib.boxc.fcrepo.exceptions; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author bbpennel + */ +public class RangeNotSatisfiableExceptionTest { + @Test + public void testConstructorWithMessage() { + var ex = new RangeNotSatisfiableException("Bad range"); + assertEquals("Bad range", ex.getMessage()); + assertNull(ex.getCause()); + } + + @Test + public void testConstructorWithThrowable() { + var causeEx = new RuntimeException(); + var ex = new RangeNotSatisfiableException(causeEx); + assertEquals(causeEx, ex.getCause()); + } + + @Test + public void testConstructorWithMessageThrowable() { + var causeEx = new RuntimeException(); + var ex = new RangeNotSatisfiableException("Bad range", causeEx); + assertEquals("Bad range", ex.getMessage()); + assertEquals(causeEx, ex.getCause()); + } +} diff --git a/indexing-solr/pom.xml b/indexing-solr/pom.xml index 472cdf80cb..3d64062c71 100644 --- a/indexing-solr/pom.xml +++ b/indexing-solr/pom.xml @@ -97,8 +97,7 @@ org.apache.solr - solr-dataimporthandler - test + solr-api org.apache.solr diff --git a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/action/BaseEmbeddedSolrTest.java b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/action/BaseEmbeddedSolrTest.java index 5d9cea8590..2950171b28 100644 --- a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/action/BaseEmbeddedSolrTest.java +++ b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/action/BaseEmbeddedSolrTest.java @@ -38,9 +38,8 @@ public void setUp() throws Exception { dataDir.mkdir(); System.setProperty("solr.data.dir", dataDir.getAbsolutePath()); - container = CoreContainer.createAndLoad(Paths.get("../etc/solr-config"), - Paths.get("../etc/solr-config/solr.xml")); - container.load(); + container = CoreContainer.createAndLoad(Paths.get("../etc/solr-config").toAbsolutePath(), + Paths.get("../etc/solr-config/solr.xml").toAbsolutePath()); server = new EmbeddedSolrServer(container, "access"); diff --git a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentTypeFilterTest.java b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentTypeFilterTest.java index 8dc7c0ca50..0397260a00 100644 --- a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentTypeFilterTest.java +++ b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentTypeFilterTest.java @@ -26,7 +26,6 @@ import edu.unc.lib.boxc.search.solr.models.IndexDocumentBean; import edu.unc.lib.boxc.search.solr.responses.SearchResultResponse; import edu.unc.lib.boxc.search.solr.services.SolrSearchService; -import org.apache.commons.collections.CollectionUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -171,9 +170,9 @@ public void testNotWorkAndNotFileObject() throws Exception { filter.filter(dip); - assertTrue(CollectionUtils.isEmpty(idb.getFileFormatType())); - assertTrue(CollectionUtils.isEmpty(idb.getFileFormatDescription())); - assertTrue(CollectionUtils.isEmpty(idb.getFileFormatCategory())); + assertNull(idb.getFileFormatType()); + assertNull(idb.getFileFormatDescription()); + assertNull(idb.getFileFormatCategory()); } @Test @@ -335,9 +334,9 @@ public void testWorkWithNoFiles() throws Exception { filter.filter(dip); - assertTrue(CollectionUtils.isEmpty(idb.getFileFormatType())); - assertTrue(CollectionUtils.isEmpty(idb.getFileFormatDescription())); - assertTrue(CollectionUtils.isEmpty(idb.getFileFormatCategory())); + assertTrue(idb.getFileFormatType().isEmpty()); + assertTrue(idb.getFileFormatDescription().isEmpty()); + assertTrue(idb.getFileFormatCategory().isEmpty()); } @Test diff --git a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/test/TestCorpus.java b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/test/TestCorpus.java index 598d975cc1..91a7a099c9 100644 --- a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/test/TestCorpus.java +++ b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/test/TestCorpus.java @@ -115,8 +115,8 @@ public List populate() { newDoc.addField("ancestorPath", makeAncestorPath(pid1, pid2, pid6)); newDoc.addField("resourceType", ResourceType.File.name()); List imgDatastreams = Arrays.asList( - ORIGINAL_FILE.getId() + "|image/png|file.png|png|766|urn:sha1:checksum|", - DatastreamType.THUMBNAIL_LARGE.getId() + "|image/png|thumb|png|55||"); + ORIGINAL_FILE.getId() + "|image/png|file.png|png|766|urn:sha1:checksum|1200x1200", + DatastreamType.JP2_ACCESS_COPY.getId() + "|image/jp2|bunny.jp2|jp2|||1200x1200"); newDoc.addField(SearchFieldKey.DATASTREAM.getSolrField(), imgDatastreams); newDoc.addField(SearchFieldKey.FILE_FORMAT_CATEGORY.getSolrField(), ContentCategory.image.getDisplayName()); newDoc.addField(SearchFieldKey.FILE_FORMAT_TYPE.getSolrField(), "png"); diff --git a/integration/pom.xml b/integration/pom.xml index 9f943b359b..3c7ee929c0 100644 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -47,67 +47,6 @@ - - com.googlecode.maven-download-plugin - download-maven-plugin - 1.6.8 - - - install-solr - pre-integration-test - - wget - - - - - https://www.apache.org/dyn/closer.lua/lucene/solr/${solr.version}/solr-${solr.version}.tgz?action=download - true - ${project.build.directory}/solr/ - - - - org.codehaus.mojo - exec-maven-plugin - - - Start solr - - exec - - pre-integration-test - - true - ${project.build.directory}/solr/solr-${solr.version}/bin/solr - start -p 48983 -s ${project.build.directory}/../../etc/solr-config/ - - - - Stop solr - - exec - - post-integration-test - - true - ${project.build.directory}/solr/solr-${solr.version}/bin/solr - - stop - -p - 48983 - - - - - - - - solr.solr.home - ${project.build.directory}/../../etc/solr-config/ - - - - org.eclipse.jetty jetty-maven-plugin diff --git a/integration/src/test/resources/spring-test/solr-embedded-context.xml b/integration/src/test/resources/spring-test/solr-embedded-context.xml index 20c4031e1e..5c6b684b49 100644 --- a/integration/src/test/resources/spring-test/solr-embedded-context.xml +++ b/integration/src/test/resources/spring-test/solr-embedded-context.xml @@ -50,8 +50,8 @@ - - - + + + \ No newline at end of file diff --git a/model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/FedoraException.java b/model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/FedoraException.java index 7e435ddabc..9039f70ce1 100644 --- a/model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/FedoraException.java +++ b/model-api/src/main/java/edu/unc/lib/boxc/model/api/exceptions/FedoraException.java @@ -8,11 +8,11 @@ public class FedoraException extends RuntimeException { private static final long serialVersionUID = 7276162681909269101L; - public FedoraException(Exception e) { + public FedoraException(Throwable e) { super(e); } - public FedoraException(String message, Exception e) { + public FedoraException(String message, Throwable e) { super(message, e); } diff --git a/model-fcrepo/pom.xml b/model-fcrepo/pom.xml index 31ba263d5e..8190903470 100644 --- a/model-fcrepo/pom.xml +++ b/model-fcrepo/pom.xml @@ -40,6 +40,10 @@ com.google.guava guava + + commons-codec + commons-codec + org.junit.jupiter diff --git a/persistence/pom.xml b/persistence/pom.xml index 9dc1e5646f..e1ab5aa89d 100644 --- a/persistence/pom.xml +++ b/persistence/pom.xml @@ -103,9 +103,5 @@ test test-jar - - commons-codec - commons-codec - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 90476ff3ad..b3da161ff3 100644 --- a/pom.xml +++ b/pom.xml @@ -79,8 +79,7 @@ 5.0.0 5.4.1 4.0.3 - - 2.32.0 + 2.35.2 2.13.2 2.13.4.2 @@ -96,7 +95,7 @@ 4.5.13 1.26.0 - 1.11 + 1.17.1 2.7 1.6 3.8.1 @@ -126,7 +125,7 @@ 2.3.6 3.10.0 - 8.10.1 + 9.6.1 5.0.0 4.4.0 @@ -694,7 +693,7 @@ com.github.tomakehurst - wiremock-jre8 + wiremock-jre8-standalone ${wiremock.version} test @@ -938,9 +937,8 @@ org.apache.solr - solr-dataimporthandler + solr-api ${solr.version} - test org.apache.solr diff --git a/search-solr/src/test/java/edu/unc/lib/boxc/search/solr/test/BaseEmbeddedSolrTest.java b/search-solr/src/test/java/edu/unc/lib/boxc/search/solr/test/BaseEmbeddedSolrTest.java index 9e74d46900..bef16ace35 100644 --- a/search-solr/src/test/java/edu/unc/lib/boxc/search/solr/test/BaseEmbeddedSolrTest.java +++ b/search-solr/src/test/java/edu/unc/lib/boxc/search/solr/test/BaseEmbeddedSolrTest.java @@ -45,9 +45,8 @@ public void setUp() throws Exception { Files.createDirectory(dataBaseDir.resolve("dataDir")); System.setProperty("solr.data.dir", dataDir.getAbsolutePath()); - container = CoreContainer.createAndLoad(Paths.get("../etc/solr-config"), - Paths.get("../etc/solr-config/solr.xml")); - container.load(); + container = CoreContainer.createAndLoad(Paths.get("../etc/solr-config").toAbsolutePath(), + Paths.get("../etc/solr-config/solr.xml").toAbsolutePath()); server = new EmbeddedSolrServer(container, "access"); @@ -86,6 +85,7 @@ protected void index(List docs) throws Exception { @AfterEach public void tearDown() throws Exception { + container.shutdown(); server.close(); log.debug("Cleaning up data directory"); FileUtils.deleteDirectory(dataDir); diff --git a/services-camel-app/pom.xml b/services-camel-app/pom.xml index aab41a0f5b..0b655d16ac 100644 --- a/services-camel-app/pom.xml +++ b/services-camel-app/pom.xml @@ -226,7 +226,7 @@ com.github.tomakehurst - wiremock-jre8 + wiremock-jre8-standalone @@ -281,10 +281,6 @@ org.junit.jupiter junit-jupiter-api - - com.github.tomakehurst - wiremock-jre8 - diff --git a/services-camel-app/src/main/java/edu/unc/lib/boxc/services/camel/streaming/StreamingPropertiesRequestProcessor.java b/services-camel-app/src/main/java/edu/unc/lib/boxc/services/camel/streaming/StreamingPropertiesRequestProcessor.java index f71e683037..7c37718cd2 100644 --- a/services-camel-app/src/main/java/edu/unc/lib/boxc/services/camel/streaming/StreamingPropertiesRequestProcessor.java +++ b/services-camel-app/src/main/java/edu/unc/lib/boxc/services/camel/streaming/StreamingPropertiesRequestProcessor.java @@ -14,7 +14,7 @@ import edu.unc.lib.boxc.operations.jms.streaming.StreamingPropertiesRequestSerializationHelper; import org.apache.camel.Exchange; import org.apache.camel.Processor; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import java.io.IOException; import java.util.Objects; diff --git a/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml b/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml index 7a2d06a562..266a4daf25 100644 --- a/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml +++ b/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml @@ -116,9 +116,9 @@ - - - + + + diff --git a/static/css/admin/performance_visualizations.css b/static/css/admin/performance_visualizations.css deleted file mode 100644 index 13e35a98a5..0000000000 --- a/static/css/admin/performance_visualizations.css +++ /dev/null @@ -1,148 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -/*! - * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=cbceccffe1cdf35233f54f998df5b04c) - * Config saved to config.json and https://gist.github.com/cbceccffe1cdf35233f54f998df5b04c - *//*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover,a.text-primary:focus{color:#286090}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#31708f}a.text-info:hover,a.text-info:focus{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover,a.text-warning:focus{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover,a.bg-primary:focus{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}} - -.col-md-12 { - padding: 0; -} -#loader { - margin-top: 45px; - font-size: 4em; -} - -.dim { - opacity: 0; -} - -.axis path, -.axis line { - fill: none; - padding: 5px; - shape-rendering: crispEdges; - stroke: gray; -} - -text { - fill: gray; -} - -text.label { - font-size: 85%; -} - -div.tooltip { - position: absolute; - text-align: left; - width: 275px; - height: auto; - padding: 15px 12px 5px 12px; - fill: black; - background: rgba(0, 0, 0, 0.9); - border: 0px; - border-radius: 8px; - pointer-events: none; - line-height: 1; - font-weight: bold; - color:white; -} - -.smaller, -.smaller li { - font-weight: normal; -} - -h5.smaller { - margin-top: -5px; -} - -circle, -#throughput-date-line { - stroke: orange; - fill: none; - pointer-events: all; -} - -#throughput-date-trend-line { - pointer-events: none; -} - -#files-by-day circle, -#files-by-ingest circle, -#files-by-day-line { - stroke: #175455; -} - - -#duration-date circle, -#duration-total-date circle, -#duration-total-date-line { - stroke: #e7298a; -} - -#total-deposits-date-line { - stroke: #000; -} - -#moves-date circle, -#moves-date-line { - stroke: #0570b0; -} - -#enh-date circle, -#enh-date-line { - stroke: #238b45; -} - -#failed-enh-date circle, -#failed-deposits-date circle, -#failed-enh-date-line, -#failed-deposits-date-line{ - stroke: #de2d26; -} - -.clicked { - color: #333; - border-color: #adadad; - background-color: #e6e6e6; -} - -ul.columns { - -webkit-columns: 50px 2; /* Chrome, Safari, Opera */ - -moz-columns: 50px 2; /* Firefox */ - columns: 50px 2; -} - -.heading { - font-weight: bolder; - font-size: 14px; -} - -/* Overlay */ -.overlay { - fill: none; - stroke: none; - pointer-events: all; -} - -.focus line { - stroke: slategray; - stroke-width: 2.5; -} - -/* Brushing */ -.brush .extent { - stroke: #fff; - fill-opacity: .125; - shape-rendering: crispEdges; -} \ No newline at end of file diff --git a/static/js/admin/src/action/RefreshResultAction.js b/static/js/admin/src/action/RefreshResultAction.js index e038bcebfd..79d04c21a4 100644 --- a/static/js/admin/src/action/RefreshResultAction.js +++ b/static/js/admin/src/action/RefreshResultAction.js @@ -52,7 +52,7 @@ define('RefreshResultAction', ['jquery', 'RemoteStateChangeMonitor'], function($ url : "/services/api/status/item/" + resultObject.pid + "/solrRecord/version", dataType : 'json' }, - maxAttempts : this.context.maxAttempts? this.context.maxAttempts : 0 + maxAttempts : this.context.maxAttempts? this.context.maxAttempts : 15 }); followupMonitor.performPing(); diff --git a/web-access-app/src/main/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentController.java b/web-access-app/src/main/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentController.java index ae59b22654..a2a620e9a6 100644 --- a/web-access-app/src/main/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentController.java +++ b/web-access-app/src/main/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentController.java @@ -3,6 +3,7 @@ import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.fcrepo.exceptions.RangeNotSatisfiableException; import edu.unc.lib.boxc.model.api.exceptions.InvalidPidException; import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; import edu.unc.lib.boxc.model.api.exceptions.ObjectTypeMismatchException; @@ -27,6 +28,8 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import javax.servlet.http.HttpServletRequest; @@ -157,8 +160,13 @@ public ResponseEntity handleObjectTypeMismatchException() { } @ExceptionHandler(value = { RuntimeException.class }) - public ResponseEntity handleUncaught(RuntimeException ex) { - log.error("Uncaught exception while streaming content", ex); + public ResponseEntity handleUncaught(RuntimeException ex, WebRequest request) { + var headers = new StringBuilder(); + request.getHeaderNames().forEachRemaining(header -> { + headers.append("\n").append(header).append(" = ").append(request.getHeader(header)); + }); + var requestUri = ((ServletWebRequest) request).getRequest().getRequestURI(); + log.error("Uncaught exception while streaming content from {} headers {}", requestUri, headers, ex); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } @@ -168,6 +176,11 @@ public ResponseEntity handleArgumentTypeMismatch(RuntimeException ex) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } + @ExceptionHandler(RangeNotSatisfiableException.class) + public ResponseEntity handleRangeNotSatisfiable() { + return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); + } + public void setFedoraContentService(FedoraContentService fedoraContentService) { this.fedoraContentService = fedoraContentService; } diff --git a/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerIT.java b/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerIT.java index c7690f644e..142aabfa35 100644 --- a/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerIT.java +++ b/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerIT.java @@ -130,6 +130,27 @@ public void testGetDatastreamWithRange() throws Exception { assertEquals("inline; filename=\"file.txt\"", response.getHeader(CONTENT_DISPOSITION)); } + @Test + public void testGetDatastreamWithRangeEndingSameAsSize() throws Exception { + PID filePid = makePid(); + + FileObject fileObj = repositoryObjectFactory.createFileObject(filePid, null); + fileObj.addOriginalFile(makeContentUri(originalPid(fileObj), BINARY_CONTENT), "file.txt", "text/plain", null, null); + + MvcResult result = mvc.perform(get("/content/" + filePid.getId()) + .header(RANGE,"bytes=0-14")) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + // Verify content was retrieved + MockHttpServletResponse response = result.getResponse(); + assertEquals(BINARY_CONTENT, response.getContentAsString()); + + assertEquals(BINARY_CONTENT.length(), response.getContentAsString().length()); + assertEquals("text/plain", response.getContentType()); + assertEquals("inline; filename=\"file.txt\"", response.getHeader(CONTENT_DISPOSITION)); + } + @Test public void testGetDatastreamDownload() throws Exception { PID filePid = makePid(); @@ -277,6 +298,19 @@ private void testGetMultipleDatastreamsWithRange(String requestPath) throws Exce assertEquals("inline; filename=\"fits.xml\"", response.getHeader(CONTENT_DISPOSITION)); } + @Test + public void testRangeExceedsFileLength() throws Exception { + PID filePid = makePid(); + FileObject fileObj = repositoryObjectFactory.createFileObject(filePid, null); + fileObj.addOriginalFile(makeContentUri(originalPid(fileObj), BINARY_CONTENT), null, "text/plain", null, null); + + mvc.perform(get("/indexablecontent/" + filePid.getId()) + .header(RANGE,"bytes=900000-900000")) + .andExpect(status().isRequestedRangeNotSatisfiable()) + .andReturn(); + } + + @Test public void testGetAdministrativeDatastreamNoPermissions() throws Exception { PID filePid = makePid(); diff --git a/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerTest.java b/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerTest.java index 0ac25d7521..622cd963bf 100644 --- a/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerTest.java +++ b/web-access-app/src/test/java/edu/unc/lib/boxc/web/access/controllers/FedoraContentControllerTest.java @@ -104,4 +104,15 @@ public void getContentIOExceptionTest() throws Exception { .andExpect(status().isBadRequest()) .andReturn(); } + + @Test + public void getContentUncaughtExceptionTest() throws Exception { + PID pid = TestHelper.makePid(); + doThrow(new RuntimeException("Uncaught")) + .when(fedoraContentService) + .streamData(any(), any(), anyBoolean(), any(), any()); + mvc.perform(get("/content/" + pid.getId()).header("Range", "bad")) + .andExpect(status().isInternalServerError()) + .andReturn(); + } } diff --git a/web-common/pom.xml b/web-common/pom.xml index c87a93b0ac..fd0ae16815 100644 --- a/web-common/pom.xml +++ b/web-common/pom.xml @@ -118,8 +118,7 @@ com.github.tomakehurst - wiremock-jre8 - test + wiremock-jre8-standalone edu.unc.lib.cdr diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java index b74939bcff..aa8d549793 100644 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java +++ b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java @@ -172,7 +172,7 @@ public String getThumbnailId(ContentObjectRecord contentObjectRecord, AccessGrou // Limit query to just children which have a thumbnail datastream var searchState = request.getSearchState(); searchState.addFilter( - QueryFilterFactory.createFilter(SearchFieldKey.DATASTREAM, DatastreamType.THUMBNAIL_LARGE)); + QueryFilterFactory.createFilter(SearchFieldKey.DATASTREAM, DatastreamType.JP2_ACCESS_COPY)); var resp = solrSearchService.getSearchResults(request); if (resp.getResultCount() > 0) { diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/DerivativeContentService.java b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/DerivativeContentService.java index e304b1c2f5..f1c6651e28 100644 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/DerivativeContentService.java +++ b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/DerivativeContentService.java @@ -9,6 +9,7 @@ import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; @@ -23,6 +24,10 @@ import edu.unc.lib.boxc.model.fcrepo.services.DerivativeService; import edu.unc.lib.boxc.model.fcrepo.services.DerivativeService.Derivative; import edu.unc.lib.boxc.web.common.exceptions.ResourceNotFoundException; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; /** * Streams content for derivative files of repository objects. @@ -59,19 +64,13 @@ public DerivativeContentService() { public void streamData(PID pid, String dsName, AccessGroupSet principals, boolean asAttachment, HttpServletResponse response) throws IOException, ResourceNotFoundException { - DatastreamType derivType = getByIdentifier(dsName); - if (derivType == null || !listDerivativeTypes().contains(derivType)) { - throw new IllegalArgumentException(dsName + " is not a valid derivative type."); - } + DatastreamType derivType = getType(dsName); accessControlService.assertHasAccess("Insufficient permissions to access derivative " + dsName + " for object " + pid, pid, principals, getPermissionForDatastream(derivType)); - Derivative deriv = derivativeService.getDerivative(pid, derivType); - if (deriv == null) { - throw new ResourceNotFoundException("Derivative " + dsName + " does not exist for object " + pid); - } + Derivative deriv = getDerivative(pid, dsName, derivType); File derivFile = deriv.getFile(); response.setHeader(CONTENT_LENGTH, Long.toString(derivFile.length())); @@ -87,6 +86,45 @@ public void streamData(PID pid, String dsName, AccessGroupSet principals, boolea IOUtils.copy(new FileInputStream(derivFile), outStream, BUFFER_SIZE); } + /** + * Returns a response entity consisting of the derivative stream used for thumbnails + * @param pid PID of the repository object + * @param dsName datastream name + * @return + * @throws FileNotFoundException + */ + public ResponseEntity streamThumbnail(PID pid, String dsName) throws FileNotFoundException { + var datastreamType = getType(dsName); + Derivative derivative = getDerivative(pid, dsName, datastreamType); + + var file = derivative.getFile(); + var filename = file.getName(); + var input = new FileInputStream(file); + InputStreamResource resource = new InputStreamResource(input); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + filename) + .contentLength(file.length()) + .contentType(MediaType.IMAGE_PNG) + .body(resource); + + } + + private DatastreamType getType(String dsName) { + DatastreamType derivType = getByIdentifier(dsName); + if (derivType == null || !listDerivativeTypes().contains(derivType)) { + throw new IllegalArgumentException(dsName + " is not a valid derivative type."); + } + return derivType; + } + + private Derivative getDerivative (PID pid, String dsName, DatastreamType datastreamType) { + Derivative derivative = derivativeService.getDerivative(pid, datastreamType); + if (derivative == null) { + throw new ResourceNotFoundException("Derivative " + dsName + " does not exist for object " + pid); + } + return derivative; + } + /** * @param derivativeService the derivativeService to set */ diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/FedoraContentService.java b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/FedoraContentService.java index f5f445910b..99465ec4ab 100644 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/FedoraContentService.java +++ b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/FedoraContentService.java @@ -96,6 +96,9 @@ public void streamData(PID pid, String dsName, boolean asAttachment, binObj = repositoryObjectLoader.getBinaryObject(dsPid); } + // Make sure the range is valid or will produce a reasonable response from fedora + range = correctRangeValue(range, binObj); + response.setHeader(CONTENT_TYPE, binObj.getMimetype()); String binaryName = binObj.getFilename(); String filename = binaryName == null ? pid.getId() : binaryName; @@ -116,6 +119,25 @@ public void streamData(PID pid, String dsName, boolean asAttachment, } } + private String correctRangeValue(String range, BinaryObject binObj) { + if (range == null) { + return null; + } + var rangeParts = range.split("-"); + if (rangeParts.length == 2) { + var endingRange = rangeParts[1]; + try { + var endingValue = Long.parseLong(endingRange); + if (endingValue >= binObj.getFilesize()) { + return rangeParts[0] + "-"; + } + } catch (NumberFormatException e) { + LOG.debug("Invalid range ending provided: {}", e.getMessage()); + } + } + return range; + } + public void streamEventLog(PID pid, AccessGroupSet principals, boolean asAttachment, HttpServletResponse response) throws IOException { diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/utils/AnalyticsTrackerUtil.java b/web-common/src/main/java/edu/unc/lib/boxc/web/common/utils/AnalyticsTrackerUtil.java index e4b8acd48e..99330760f4 100644 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/utils/AnalyticsTrackerUtil.java +++ b/web-common/src/main/java/edu/unc/lib/boxc/web/common/utils/AnalyticsTrackerUtil.java @@ -5,7 +5,7 @@ import edu.unc.lib.boxc.search.api.models.ContentObjectRecord; import edu.unc.lib.boxc.search.api.requests.SimpleIdRequest; import edu.unc.lib.boxc.search.solr.services.SolrSearchService; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.matomo.java.tracking.MatomoRequest; import org.matomo.java.tracking.MatomoTracker; import org.matomo.java.tracking.TrackerConfiguration; @@ -17,7 +17,6 @@ import javax.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; import java.net.URI; -import java.util.Random; /** * Utility for performing asynchronous analytics tracking events when unable to use the javascript api diff --git a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java index e44233303c..2203762a56 100644 --- a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java +++ b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java @@ -270,7 +270,7 @@ public void noPrimaryObjThumbnailMultipleFiles() { assertEquals(noOriginalFileObj.getId(), accessCopiesService.getThumbnailId(noOriginalFileObj, principals, false)); // Gets the ID of the specific child with a thumbnail assertEquals(mdObjectImg.getId(), accessCopiesService.getThumbnailId(noOriginalFileObj, principals, true)); - assertRequestedDatastreamFilter(DatastreamType.THUMBNAIL_LARGE); + assertRequestedDatastreamFilter(DatastreamType.JP2_ACCESS_COPY); assertSortType("default"); } @@ -311,7 +311,7 @@ public void getThumbnailIdNoPrimaryMultipleImages() { // Gets the ID of the specific child with a thumbnail assertEquals(mdObjectImg2.getId(), accessCopiesService.getThumbnailId(noOriginalFileObj, principals, true)); - assertRequestedDatastreamFilter(DatastreamType.THUMBNAIL_LARGE); + assertRequestedDatastreamFilter(DatastreamType.JP2_ACCESS_COPY); assertSortType("default"); } diff --git a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/DerivativeContentServiceTest.java b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/DerivativeContentServiceTest.java new file mode 100644 index 0000000000..e739758dee --- /dev/null +++ b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/DerivativeContentServiceTest.java @@ -0,0 +1,61 @@ +package edu.unc.lib.boxc.web.common.services; + +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.fcrepo.services.DerivativeService; +import org.apache.solr.client.solrj.SolrServerException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static edu.unc.lib.boxc.model.fcrepo.test.TestHelper.makePid; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +public class DerivativeContentServiceTest { + private AutoCloseable closeable; + private DerivativeContentService derivativeContentService; + @Mock + private AccessControlService accessControlService; + @Mock + private DerivativeService derivativeService; + + @BeforeEach + public void init() { + closeable = openMocks(this); + derivativeContentService = new DerivativeContentService(); + derivativeContentService.setAccessControlService(accessControlService); + derivativeContentService.setDerivativeService(derivativeService); + } + + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + + @Test + public void testStreamThumbnail() throws FileNotFoundException { + var pid = makePid(); + var datastreamType = DatastreamType.THUMBNAIL_SMALL; + var file = new File("src/test/resources/tokki.jpg"); + var derivative = new DerivativeService.Derivative(datastreamType, file); + when(derivativeService.getDerivative(eq(pid), eq(datastreamType))).thenReturn(derivative); + + var respEntity = derivativeContentService.streamThumbnail(pid, "thumbnail_small"); + assertEquals(HttpStatus.OK, respEntity.getStatusCode()); + assertEquals("inline; filename=\"tokki.jpg\"", respEntity.getHeaders().getContentDisposition().toString()); + assertEquals(MediaType.IMAGE_PNG, respEntity.getHeaders().getContentType()); + } +} diff --git a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/FedoraContentServiceTest.java b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/FedoraContentServiceTest.java index fd97890d28..f4decde56c 100644 --- a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/FedoraContentServiceTest.java +++ b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/FedoraContentServiceTest.java @@ -1,10 +1,18 @@ package edu.unc.lib.boxc.web.common.services; import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; +import edu.unc.lib.boxc.fcrepo.exceptions.RangeNotSatisfiableException; +import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.event.PremisLog; import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.model.api.ids.PID; import edu.unc.lib.boxc.model.api.objects.BinaryObject; import edu.unc.lib.boxc.model.api.objects.FileObject; import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; +import edu.unc.lib.boxc.model.api.rdf.DcElements; +import edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids; +import org.apache.jena.rdf.model.ModelFactory; import org.fcrepo.client.FcrepoClient; import org.fcrepo.client.FcrepoOperationFailedException; import org.fcrepo.client.FcrepoResponse; @@ -19,19 +27,22 @@ import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import static edu.unc.lib.boxc.model.api.DatastreamType.ORIGINAL_FILE; import static edu.unc.lib.boxc.model.fcrepo.test.TestHelper.makePid; import static org.apache.http.HttpHeaders.CONTENT_LENGTH; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.apache.http.HttpHeaders.CONTENT_RANGE; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; +import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; public class FedoraContentServiceTest { private AutoCloseable closeable; @@ -54,14 +65,18 @@ public class FedoraContentServiceTest { private FcrepoResponse fcrepoResponse; @Mock private ServletOutputStream outputStream; + @Mock + private PremisLog premisLog; @BeforeEach - public void setup() { + public void setup() throws Exception { closeable = openMocks(this); fedoraContentService = new FedoraContentService(); fedoraContentService.setClient(fcrepoClient); fedoraContentService.setAccessControlService(accessControlService); fedoraContentService.setRepositoryObjectLoader(repositoryObjectLoader); + when(response.getOutputStream()).thenReturn(outputStream); + when(fileObject.getPremisLog()).thenReturn(premisLog); } @AfterEach @@ -90,18 +105,151 @@ public void streamDataWithExternalDatastream() { @Test public void streamDataSuccess() throws IOException, FcrepoOperationFailedException { var pid = makePid(); - when(repositoryObjectLoader.getFileObject(eq(pid))).thenReturn(fileObject); - when(fileObject.getOriginalFile()).thenReturn(binaryObject); - when(binaryObject.getFilename()).thenReturn("Best Name"); - when(binaryObject.getPid()).thenReturn(pid); - when(fcrepoClient.get(any())).thenReturn(builder); - when(builder.perform()).thenReturn(fcrepoResponse); + mockWithOriginalFile(pid); when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("image".getBytes(StandardCharsets.UTF_8))); - when(response.getOutputStream()).thenReturn(outputStream); when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("5"); fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, null); verify(response).setHeader(CONTENT_LENGTH, "5"); + verify(response).setHeader(CONTENT_DISPOSITION, "inline; filename=\"Best Name\""); + } + + @Test + public void streamDataSuccessAsAttachment() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + mockWithOriginalFile(pid); + when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("image".getBytes(StandardCharsets.UTF_8))); + when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("5"); + + fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), true, response, null); + + verify(response).setHeader(CONTENT_LENGTH, "5"); + verify(response).setHeader(CONTENT_DISPOSITION, "attachment; filename=\"Best Name\""); + } + + @Test + public void streamDataSuccessOtherDatastream() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + var modsPid = DatastreamPids.getMdDescriptivePid(pid); + mockWithOriginalFile(pid); + var modsBinary = mock(BinaryObject.class); + when(modsBinary.getFilename()).thenReturn("mods.xml"); + when(modsBinary.getPid()).thenReturn(modsPid); + when(modsBinary.getFilesize()).thenReturn(4L); + when(repositoryObjectLoader.getBinaryObject(eq(modsPid))).thenReturn(modsBinary); + when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("desc".getBytes(StandardCharsets.UTF_8))); + when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("4"); + + fedoraContentService.streamData(pid, DatastreamType.MD_DESCRIPTIVE.getId(), false, response, null); + + verify(response).setHeader(CONTENT_LENGTH, "4"); + verify(response).setHeader(CONTENT_DISPOSITION, "inline; filename=\"mods.xml\""); + } + + @Test + public void streamDataWithValidRange() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + mockWithOriginalFile(pid); + when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("imag".getBytes(StandardCharsets.UTF_8))); + when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("4"); + var contentRange = "bytes 0-3/5"; + when(fcrepoResponse.getHeaderValue(CONTENT_RANGE)).thenReturn(contentRange); + + fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=0-3"); + + verify(response).setHeader(CONTENT_LENGTH, "4"); + verify(response).setHeader(CONTENT_RANGE, contentRange); + verify(builder).addHeader("Range", "bytes=0-3"); + } + + @Test + public void streamDataWithEndRangeSameAsSize() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + mockWithOriginalFile(pid); + when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("image".getBytes(StandardCharsets.UTF_8))); + when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("5"); + var contentRange = "bytes 0-4/5"; + when(fcrepoResponse.getHeaderValue(CONTENT_RANGE)).thenReturn(contentRange); + + fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=0-5"); + + verify(response).setHeader(CONTENT_LENGTH, "5"); + verify(response).setHeader(CONTENT_RANGE, contentRange); + verify(builder).addHeader("Range", "bytes=0-"); + } + + @Test + public void streamDataWithEndRangeGreaterThanSize() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + mockWithOriginalFile(pid); + when(fcrepoResponse.getBody()).thenReturn(new ByteArrayInputStream("mage".getBytes(StandardCharsets.UTF_8))); + when(fcrepoResponse.getHeaderValue(CONTENT_LENGTH)).thenReturn("4"); + var contentRange = "bytes 1-4/5"; + when(fcrepoResponse.getHeaderValue(CONTENT_RANGE)).thenReturn(contentRange); + + fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=1-8"); + + verify(response).setHeader(CONTENT_LENGTH, "4"); + verify(response).setHeader(CONTENT_RANGE, contentRange); + verify(builder).addHeader("Range", "bytes=1-"); + } + + @Test + public void streamDataWithEndRangeInvalid() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + mockWithOriginalFile(pid); + when(builder.perform()).thenThrow(new FcrepoOperationFailedException(null, 416, "Bad Range")); + when(fcrepoResponse.getStatusCode()).thenReturn(416); + + assertThrows(RangeNotSatisfiableException.class, () -> { + fedoraContentService.streamData(pid, ORIGINAL_FILE.getId(), false, response, "bytes=1-bad"); + }); + } + + @Test + public void streamEventLogTest() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + + when(repositoryObjectLoader.getRepositoryObject(eq(pid))).thenReturn(fileObject); + var model = ModelFactory.createDefaultModel(); + when(premisLog.getEventsModel()).thenReturn(model); + var logResource = model.getResource(pid.getRepositoryPath()); + logResource.addProperty(DcElements.title, "test title"); + + var principals = new AccessGroupSetImpl("group"); + + fedoraContentService.streamEventLog(pid, principals, false, response); + + verify(response).setHeader(CONTENT_TYPE, "text/turtle"); + verify(response).setHeader(CONTENT_DISPOSITION, "inline; filename=\"" + pid.getId() + "_event_log.ttl\""); + } + + @Test + public void streamEventLogAttachmentTest() throws IOException, FcrepoOperationFailedException { + var pid = makePid(); + when(repositoryObjectLoader.getRepositoryObject(eq(pid))).thenReturn(fileObject); + + var model = ModelFactory.createDefaultModel(); + when(premisLog.getEventsModel()).thenReturn(model); + var logResource = model.getResource(pid.getRepositoryPath()); + logResource.addProperty(DcElements.title, "test title"); + + var principals = new AccessGroupSetImpl("group"); + + fedoraContentService.streamEventLog(pid, principals, true, response); + + verify(response).setHeader(CONTENT_TYPE, "text/turtle"); + verify(response).setHeader(CONTENT_DISPOSITION, "attachment; filename=\"" + pid.getId() + "_event_log.ttl\""); + } + + private void mockWithOriginalFile(PID pid) throws FcrepoOperationFailedException { + when(repositoryObjectLoader.getFileObject(eq(pid))).thenReturn(fileObject); + when(fileObject.getOriginalFile()).thenReturn(binaryObject); + when(binaryObject.getFilename()).thenReturn("Best Name"); + when(binaryObject.getPid()).thenReturn(pid); + when(binaryObject.getFilesize()).thenReturn(5L); + when(fcrepoClient.get(any())).thenReturn(builder); + when(builder.perform()).thenReturn(fcrepoResponse); } } diff --git a/web-common/src/test/resources/tokki.jpg b/web-common/src/test/resources/tokki.jpg new file mode 100755 index 0000000000..1512834e01 Binary files /dev/null and b/web-common/src/test/resources/tokki.jpg differ diff --git a/web-services-app/pom.xml b/web-services-app/pom.xml index 9953f983e9..b8240665eb 100644 --- a/web-services-app/pom.xml +++ b/web-services-app/pom.xml @@ -163,8 +163,7 @@ com.github.tomakehurst - wiremock-jre8 - test + wiremock-jre8-standalone info.freelibrary diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/DownloadImageService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/DownloadImageService.java index d8ff63c458..58a411e5fd 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/DownloadImageService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/DownloadImageService.java @@ -36,7 +36,7 @@ public class DownloadImageService { * @return a response entity which contains headers and content of the access copy image * @throws IOException */ - public ResponseEntity streamImage(ContentObjectRecord contentObjectRecord, String size) + public ResponseEntity streamImage(ContentObjectRecord contentObjectRecord, String size, boolean attachment) throws IOException { if (contentObjectRecord.getDatastreamObject(DatastreamType.JP2_ACCESS_COPY.getId()) == null) { return ResponseEntity.notFound().build(); @@ -47,9 +47,10 @@ public ResponseEntity streamImage(ContentObjectRecord conte InputStream input = new URL(url).openStream(); InputStreamResource resource = new InputStreamResource(input); String filename = getDownloadFilename(contentObjectRecord, size); + var type = attachment ? "attachment" : "inline"; return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename) + .header(HttpHeaders.CONTENT_DISPOSITION, type + "; filename=" + filename) .contentType(MediaType.IMAGE_JPEG) .body(resource); } diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java index ee646c820f..6a8a21733e 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java @@ -31,7 +31,7 @@ import info.freelibrary.iiif.presentation.v3.properties.RequiredStatement; import info.freelibrary.iiif.presentation.v3.properties.ViewingDirection; import info.freelibrary.iiif.presentation.v3.services.ImageService3; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DatastreamController.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DatastreamController.java index f64b25197a..a120054935 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DatastreamController.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DatastreamController.java @@ -1,5 +1,6 @@ package edu.unc.lib.boxc.web.services.rest; +import edu.unc.lib.boxc.auth.api.Permission; import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.services.AccessControlService; @@ -17,11 +18,14 @@ import edu.unc.lib.boxc.web.common.services.FedoraContentService; import edu.unc.lib.boxc.web.common.services.SolrQueryLayerService; import edu.unc.lib.boxc.web.common.utils.AnalyticsTrackerUtil; +import edu.unc.lib.boxc.web.services.processing.DownloadImageService; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -36,6 +40,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.Map; import static edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil.getPermissionForDatastream; import static edu.unc.lib.boxc.auth.fcrepo.services.GroupsThreadStore.getAgentPrincipals; @@ -53,6 +59,9 @@ @Controller public class DatastreamController { private static final Logger log = LoggerFactory.getLogger(DatastreamController.class); + private static final String SMALL = "small"; + private static final String LARGE = "large"; + private static final Map THUMB_SIZE_MAP = Map.of(SMALL, 64, LARGE, 128); @Autowired private FedoraContentService fedoraContentService; @@ -66,6 +75,8 @@ public class DatastreamController { private AccessCopiesService accessCopiesService; @Autowired private AccessControlService accessControlService; + @Autowired + private DownloadImageService downloadImageService; private static final List THUMB_QUERY_FIELDS = Arrays.asList( SearchFieldKey.ID.name(), SearchFieldKey.DATASTREAM.name(), @@ -116,21 +127,25 @@ private void recordDownloadEvent(PID pid, String datastream, AccessGroupSet prin } @RequestMapping("/thumb/{pid}") - public void getThumbnailSmall(@PathVariable("pid") String pid, - @RequestParam(value = "size", defaultValue = "small") String size, HttpServletRequest request, - HttpServletResponse response) throws IOException { + public ResponseEntity getThumbnailSmall(@PathVariable("pid") String pid, + @RequestParam(value = "size", defaultValue = SMALL) String size) { - getThumbnail(pid, size, request, response); + return getThumbnail(pid, size); } @RequestMapping("/thumb/{pid}/{size}") - public void getThumbnail(@PathVariable("pid") String pidString, - @PathVariable("size") String size, HttpServletRequest request, - HttpServletResponse response) throws IOException { + public ResponseEntity getThumbnail(@PathVariable("pid") String pidString, + @PathVariable("size") String size) { PID pid = PIDs.get(pidString); AccessGroupSet principals = getAgentPrincipals().getPrincipals(); - String thumbName = "thumbnail_" + size.toLowerCase().trim(); + accessControlService.assertHasAccess("Insufficient permissions to get thumbnail for " + pidString, + pid, principals, Permission.viewAccessCopies); + + size = size.toLowerCase(); + if (!THUMB_SIZE_MAP.containsKey(size)) { + throw new IllegalArgumentException("That is not a valid thumbnail size"); + } // For work objects, determine the source of the thumbnail var objRequest = new SimpleIdRequest(pid, THUMB_QUERY_FIELDS, principals); @@ -138,15 +153,37 @@ public void getThumbnail(@PathVariable("pid") String pidString, if (objRecord == null) { throw new ResourceNotFoundException("The requested object either does not exist or is not accessible"); } - if (ResourceType.Work.name().equals(objRecord.getResourceType())) { + + if (ResourceType.Folder.name().equals(objRecord.getResourceType()) || + ResourceType.Collection.name().equals(objRecord.getResourceType()) || + ResourceType.AdminUnit.name().equals(objRecord.getResourceType())) { + String thumbName = "thumbnail_" + size.toLowerCase().trim(); + try { + return derivativeContentService.streamThumbnail(pid, thumbName); + } catch (IOException e) { + log.error("Error streaming thumbnail for {}", pid); + } + } else if (ResourceType.Work.name().equals(objRecord.getResourceType())) { var thumbId = accessCopiesService.getThumbnailId(objRecord, principals, true); if (thumbId != null) { pid = PIDs.get(thumbId); + // check permissions for thumbnail file + accessControlService.assertHasAccess("Insufficient permissions to get thumbnail for " + pidString, + pid, principals, Permission.viewAccessCopies); log.debug("Got thumbnail id {} for work {}", thumbId, pidString); } } - derivativeContentService.streamData(pid, thumbName, principals, false, response); + var thumbObjRequest = new SimpleIdRequest(pid, THUMB_QUERY_FIELDS, principals); + var thumbObjRecord = solrQueryLayerService.getObjectById(thumbObjRequest); + var pixelSize = THUMB_SIZE_MAP.get(size).toString(); + + try { + return downloadImageService.streamImage(thumbObjRecord, pixelSize, false); + } catch (IOException e) { + log.error("Error streaming thumbnail for {} at size {}", pidString, pixelSize, e); + } + return new ResponseEntity<>(HttpStatus.NOT_FOUND); } @ResponseStatus(value = HttpStatus.NOT_FOUND) @@ -187,4 +224,8 @@ public void setAccessCopiesService(AccessCopiesService accessCopiesService) { public void setAccessControlService(AccessControlService accessControlService) { this.accessControlService = accessControlService; } + + public void setDownloadImageService(DownloadImageService downloadImageService) { + this.downloadImageService = downloadImageService; + } } \ No newline at end of file diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java index e5bd5dc864..e43b979f8e 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java @@ -67,7 +67,7 @@ public ResponseEntity getImage(@PathVariable("pid") String try { analyticsTracker.trackEvent(request, "download access copy", pid, principals); - return downloadImageService.streamImage(contentObjectRecord, validatedSize); + return downloadImageService.streamImage(contentObjectRecord, validatedSize, true); } catch (IOException e) { log.error("Error streaming access copy image for {} at size {}", pidString, validatedSize, e); } diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java index 2934e156da..e5d9104b53 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/exceptions/RestResponseEntityExceptionHandler.java @@ -1,5 +1,6 @@ package edu.unc.lib.boxc.web.services.rest.exceptions; +import edu.unc.lib.boxc.fcrepo.exceptions.RangeNotSatisfiableException; import edu.unc.lib.boxc.model.api.exceptions.InvalidOperationForObjectType; import java.io.EOFException; @@ -77,6 +78,13 @@ public ResponseEntity handleEofException(EOFException ex, WebRequest req return null; } + @ExceptionHandler(RangeNotSatisfiableException.class) + public ResponseEntity handleRangeNotSatisfiable(RuntimeException ex, WebRequest request) { + var rangeValue = request.getHeader(HttpHeaders.RANGE); + log.debug("Unsatisfiable range {} requested {}", rangeValue, getRequestUri(request), ex); + return handleExceptionInternal(ex, null, new HttpHeaders(), HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE, request); + } + @ExceptionHandler(value = { SolrRuntimeException.class }) protected ResponseEntity handleUnavailable(Exception ex, WebRequest request) { try { diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DatastreamRestControllerIT.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DatastreamRestControllerIT.java index d36e9f5d62..60f139c851 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DatastreamRestControllerIT.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DatastreamRestControllerIT.java @@ -1,67 +1,79 @@ package edu.unc.lib.boxc.web.services.rest; -import static edu.unc.lib.boxc.auth.api.Permission.viewAccessCopies; -import static edu.unc.lib.boxc.auth.api.Permission.viewHidden; -import static edu.unc.lib.boxc.model.api.DatastreamType.MD_EVENTS; -import static edu.unc.lib.boxc.model.api.DatastreamType.TECHNICAL_METADATA; -import static edu.unc.lib.boxc.model.api.DatastreamType.THUMBNAIL_SMALL; -import static edu.unc.lib.boxc.model.api.ids.RepositoryPathConstants.HASHED_PATH_DEPTH; -import static edu.unc.lib.boxc.model.api.ids.RepositoryPathConstants.HASHED_PATH_SIZE; -import static edu.unc.lib.boxc.model.api.rdf.RDFModelUtil.createModel; -import static edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids.getTechnicalMetadataPid; -import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.idToPath; -import static edu.unc.lib.boxc.web.common.services.FedoraContentService.CONTENT_DISPOSITION; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.MockitoAnnotations.openMocks; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; - +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; import edu.unc.lib.boxc.indexing.solr.test.TestCorpus; +import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.api.objects.FileObject; +import edu.unc.lib.boxc.model.api.objects.FolderObject; +import edu.unc.lib.boxc.model.api.rdf.Premis; +import edu.unc.lib.boxc.model.fcrepo.ids.AgentPids; +import edu.unc.lib.boxc.model.fcrepo.services.DerivativeService; +import edu.unc.lib.boxc.operations.api.events.PremisLoggerFactory; +import edu.unc.lib.boxc.operations.api.images.ImageServerUtil; import edu.unc.lib.boxc.web.common.services.AccessCopiesService; +import edu.unc.lib.boxc.web.common.services.DerivativeContentService; import edu.unc.lib.boxc.web.common.services.FedoraContentService; import edu.unc.lib.boxc.web.common.services.SolrQueryLayerService; import edu.unc.lib.boxc.web.common.utils.AnalyticsTrackerUtil; +import edu.unc.lib.boxc.web.services.processing.DownloadImageService; import edu.unc.lib.boxc.web.services.rest.exceptions.RestResponseEntityExceptionHandler; +import edu.unc.lib.boxc.web.services.rest.modify.AbstractAPIIT; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.jena.rdf.model.Model; import org.apache.jena.vocabulary.RDF; import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer; import org.fcrepo.client.FcrepoClient; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.web.servlet.MvcResult; - -import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; -import edu.unc.lib.boxc.auth.api.services.AccessControlService; -import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; -import edu.unc.lib.boxc.model.api.DatastreamType; -import edu.unc.lib.boxc.model.api.ids.PID; -import edu.unc.lib.boxc.model.api.objects.FileObject; -import edu.unc.lib.boxc.model.api.objects.FolderObject; -import edu.unc.lib.boxc.model.api.rdf.Premis; -import edu.unc.lib.boxc.model.fcrepo.ids.AgentPids; -import edu.unc.lib.boxc.model.fcrepo.services.DerivativeService; -import edu.unc.lib.boxc.operations.api.events.PremisLoggerFactory; -import edu.unc.lib.boxc.web.common.services.DerivativeContentService; -import edu.unc.lib.boxc.web.services.rest.modify.AbstractAPIIT; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static edu.unc.lib.boxc.auth.api.Permission.viewAccessCopies; +import static edu.unc.lib.boxc.auth.api.Permission.viewHidden; +import static edu.unc.lib.boxc.model.api.DatastreamType.MD_EVENTS; +import static edu.unc.lib.boxc.model.api.DatastreamType.TECHNICAL_METADATA; +import static edu.unc.lib.boxc.model.api.DatastreamType.THUMBNAIL_LARGE; +import static edu.unc.lib.boxc.model.api.DatastreamType.THUMBNAIL_SMALL; +import static edu.unc.lib.boxc.model.api.ids.RepositoryPathConstants.HASHED_PATH_DEPTH; +import static edu.unc.lib.boxc.model.api.ids.RepositoryPathConstants.HASHED_PATH_SIZE; +import static edu.unc.lib.boxc.model.api.rdf.RDFModelUtil.createModel; +import static edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids.getTechnicalMetadataPid; +import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.idToPath; +import static edu.unc.lib.boxc.web.common.services.FedoraContentService.CONTENT_DISPOSITION; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.http.HttpHeaders.RANGE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.MockitoAnnotations.openMocks; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + /** * * @author bbpennel @@ -72,6 +84,8 @@ @ContextConfiguration("/spring-test/solr-indexing-context.xml"), @ContextConfiguration("/datastream-content-it-servlet.xml") }) + +@WireMockTest(httpPort = 46887) public class DatastreamRestControllerIT extends AbstractAPIIT { private static final String BINARY_CONTENT = "binary content"; @@ -94,6 +108,8 @@ public class DatastreamRestControllerIT extends AbstractAPIIT { private FcrepoClient fcrepoClient; @Autowired private EmbeddedSolrServer embeddedSolrServer; + @Autowired + private DownloadImageService downloadImageService; private DatastreamController controller; @TempDir @@ -111,6 +127,7 @@ public void initLocal() { controller.setFedoraContentService(fedoraContentService); controller.setDerivativeContentService(derivativeContentService); controller.setAccessCopiesService(accessCopiesService); + controller.setDownloadImageService(downloadImageService); mvc = MockMvcBuilders.standaloneSetup(controller) .setControllerAdvice(new RestResponseEntityExceptionHandler()) .build(); @@ -123,6 +140,11 @@ public void initLocal() { fedoraContentService.setClient(fcrepoClient); } + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + @Test public void testGetFile() throws Exception { PID filePid = makePid(); @@ -180,36 +202,63 @@ public void testGetMultipleDatastreams() throws Exception { } @Test - public void testGetThumbnail() throws Exception { + public void testGetThumbnailWithFileObject() throws Exception { var corpus = populateCorpus(); - PID filePid = corpus.pid6File; String id = filePid.getId(); - createDerivative(id, THUMBNAIL_SMALL, BINARY_CONTENT.getBytes()); - MvcResult result = mvc.perform(get("/thumb/" + filePid.getId())) + var filename = "bunny.jpg"; + var formattedBasePath = "/iiif/v3/" + ImageServerUtil.getImageServerEncodedId(id); + stubFor(WireMock.get(urlMatching(formattedBasePath + "/full/!64,64/0/default.jpg")) + .willReturn(aResponse() + .withStatus(HttpStatus.OK.value()) + .withBody(filename) + .withHeader("Content-Type", "image/jpeg"))); + + MvcResult result = mvc.perform(get("/thumb/" + id)) .andExpect(status().is2xxSuccessful()) .andReturn(); // Verify content was retrieved MockHttpServletResponse response = result.getResponse(); - assertEquals(BINARY_CONTENT, response.getContentAsString()); - assertEquals(BINARY_CONTENT.length(), response.getContentLength()); - assertEquals("image/png", response.getContentType()); - assertEquals("inline; filename=\"" + id + "." + THUMBNAIL_SMALL.getExtension() + "\"", - response.getHeader(CONTENT_DISPOSITION)); + // TO DO assert correct image returned + assertEquals("image/jpeg", response.getContentType()); + assertEquals("inline; filename=file_64px.jpg", response.getHeader(CONTENT_DISPOSITION)); } @Test public void testGetThumbnailForWork() throws Exception { var corpus = populateCorpus(); - PID filePid = corpus.pid6File; - String id = filePid.getId(); - createDerivative(id, THUMBNAIL_SMALL, BINARY_CONTENT.getBytes()); PID workPid = corpus.pid6; - MvcResult result = mvc.perform(get("/thumb/" + workPid.getId())) + var filename = "bunny.jpg"; + var formattedBasePath = "/iiif/v3/" + ImageServerUtil.getImageServerEncodedId(filePid.getId()); + stubFor(WireMock.get(urlMatching(formattedBasePath + "/full/!128,128/0/default.jpg")) + .willReturn(aResponse() + .withStatus(HttpStatus.OK.value()) + .withBody(filename) + .withHeader("Content-Type", "image/jpeg"))); + + MvcResult result = mvc.perform(get("/thumb/" + workPid.getId() + "/large")) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + // Verify content was retrieved + MockHttpServletResponse response = result.getResponse(); + // TO DO assert correct image returned + assertEquals("image/jpeg", response.getContentType()); + assertEquals("inline; filename=file_128px.jpg", response.getHeader(CONTENT_DISPOSITION)); + } + + @Test + public void testGetThumbnailForCollection() throws Exception { + var corpus = populateCorpus(); + var collectionPid = corpus.pid2; + var id = collectionPid.getId(); + createDerivative(id, THUMBNAIL_LARGE, BINARY_CONTENT.getBytes()); + + MvcResult result = mvc.perform(get("/thumb/" + id + "/large")) .andExpect(status().is2xxSuccessful()) .andReturn(); @@ -217,18 +266,15 @@ public void testGetThumbnailForWork() throws Exception { MockHttpServletResponse response = result.getResponse(); assertEquals(BINARY_CONTENT, response.getContentAsString()); assertEquals(BINARY_CONTENT.length(), response.getContentLength()); - assertEquals("image/png", response.getContentType()); - assertEquals("inline; filename=\"" + id + "." + THUMBNAIL_SMALL.getExtension() + "\"", + assertEquals(MediaType.IMAGE_PNG.toString(), response.getContentType()); + assertEquals("inline; filename=" + id + "." + THUMBNAIL_LARGE.getExtension(), response.getHeader(CONTENT_DISPOSITION)); } @Test public void testGetInvalidThumbnailSize() throws Exception { var corpus = populateCorpus(); - PID filePid = corpus.pid6File; - String id = filePid.getId(); - createDerivative(id, THUMBNAIL_SMALL, BINARY_CONTENT.getBytes()); mvc.perform(get("/thumb/" + filePid.getId() + "/megasize")) .andExpect(status().isBadRequest()) @@ -238,10 +284,7 @@ public void testGetInvalidThumbnailSize() throws Exception { @Test public void testGetThumbnailNoPermission() throws Exception { var corpus = populateCorpus(); - PID filePid = corpus.pid6File; - String id = filePid.getId(); - createDerivative(id, THUMBNAIL_SMALL, BINARY_CONTENT.getBytes()); doThrow(new AccessRestrictionException()).when(accessControlService) .assertHasAccess(anyString(), eq(filePid), any(AccessGroupSetImpl.class), eq(viewAccessCopies)); @@ -355,7 +398,21 @@ public void testGetEventLogNoPermissions() throws Exception { assertEquals("", response.getContentAsString(), "Content must not be returned"); } - private File createDerivative(String id, DatastreamType dsType, byte[] content) throws Exception { + @Test + public void testRangeExceedsFileLength() throws Exception { + PID filePid = makePid(); + + FileObject fileObj = repositoryObjectFactory.createFileObject(filePid, null); + Path contentPath = createBinaryContent(BINARY_CONTENT); + fileObj.addOriginalFile(contentPath.toUri(), "file.txt", "text/plain", null, null); + + mvc.perform(get("/file/" + filePid.getId()) + .header(RANGE,"bytes=900000-900001")) + .andExpect(status().isRequestedRangeNotSatisfiable()) + .andReturn(); + } + + private void createDerivative(String id, DatastreamType dsType, byte[] content) throws Exception { String hashedPath = idToPath(id, HASHED_PATH_DEPTH, HASHED_PATH_SIZE); Path derivPath = Paths.get(derivDirPath, dsType.getId(), hashedPath, id + "." + dsType.getExtension()); @@ -363,7 +420,5 @@ private File createDerivative(String id, DatastreamType dsType, byte[] content) File derivFile = derivPath.toFile(); derivFile.getParentFile().mkdirs(); FileUtils.writeByteArrayToFile(derivFile, content); - - return derivFile; } } \ No newline at end of file diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/modify/ExportCsvIT.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/modify/ExportCsvIT.java index 67725683f9..affcf06034 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/modify/ExportCsvIT.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/modify/ExportCsvIT.java @@ -35,7 +35,7 @@ import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.jena.rdf.model.Model; import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer; import org.junit.jupiter.api.BeforeEach; diff --git a/web-services-app/src/test/resources/datastream-content-it-servlet.xml b/web-services-app/src/test/resources/datastream-content-it-servlet.xml index 39940333bd..c20cab5b0c 100644 --- a/web-services-app/src/test/resources/datastream-content-it-servlet.xml +++ b/web-services-app/src/test/resources/datastream-content-it-servlet.xml @@ -21,6 +21,10 @@ + + + + diff --git a/web-services-app/src/test/resources/spring-test/solr-indexing-context.xml b/web-services-app/src/test/resources/spring-test/solr-indexing-context.xml index d993856d0e..4770e5b865 100644 --- a/web-services-app/src/test/resources/spring-test/solr-indexing-context.xml +++ b/web-services-app/src/test/resources/spring-test/solr-indexing-context.xml @@ -130,9 +130,9 @@ - - - + + +