diff --git a/README.md b/README.md index 218e2cb1a7..e56029cc6a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -# DSpace +# DSpace customized for [VTechWorks](https://vtechworks.lib.vt.edu/) +[University Libraries](https://lib.vt.edu/), [Virginia Tech](https://vt.edu/) [![Build Status](https://travis-ci.org/DSpace/DSpace.png?branch=master)](https://travis-ci.org/DSpace/DSpace) diff --git a/dspace/config/input-forms.xml b/dspace/config/input-forms.xml index 82c2a183f6..855bbe3880 100644 --- a/dspace/config/input-forms.xml +++ b/dspace/config/input-forms.xml @@ -1247,10 +1247,19 @@ Choose a discipline + + Agribusiness + Agribusiness + + + Applied Animal Behavior and Welfare + Applied Animal Behavior and Welfare + Applied Nutrition and Physical Activity Applied Nutrition and Physical Activity + Education Education diff --git a/dspace/config/local.cfg b/dspace/config/local.cfg index c0aa44e976..a060ea665e 100644 --- a/dspace/config/local.cfg +++ b/dspace/config/local.cfg @@ -352,7 +352,7 @@ xmlui.theme.mirage.item-list.emphasis = file # have the appropriate privileges (add & write) on the bundle then that bundle will # not be shown to the user as an option. # Allow upload to MOVIEPOSTER bundle from web UI -xmlui.bundle.upload = ORIGINAL, METADATA, THUMBNAIL, MOVIEPOSTER, LICENSE, CC-LICENSE +xmlui.bundle.upload = ORIGINAL, METADATA, THUMBNAIL, MOVIEPOSTER, LICENSE, CC-LICENSE, SWORD ########################################################################################## # Overwritten modules files: Variable in config/modules files overwritten by local.cfg, # diff --git a/dspace/config/log4j-solr.properties b/dspace/config/log4j-solr.properties index c94e384d81..41d07b49f2 100644 --- a/dspace/config/log4j-solr.properties +++ b/dspace/config/log4j-solr.properties @@ -31,6 +31,8 @@ log4j.appender.CONSOLE.layout.ConversionPattern=%-4r [%t] %-5p %c %x \u2013 %m%n log4j.appender.file=org.apache.log4j.DailyRollingFileAppender # Set this to yyyy-MM-DD for daily log files, or yyyy-MM for monthly files log4j.appender.file.DatePattern='.'yyyy-MM-dd +# The number of log files to keep, or 0 to keep them all +log4j.appender.file.MaxLogs=60 #- File to log to and log format log4j.appender.file.File=${log.dir}/solr.log diff --git a/dspace/config/log4j.properties b/dspace/config/log4j.properties index 238d0d8fbf..02231b3dbf 100644 --- a/dspace/config/log4j.properties +++ b/dspace/config/log4j.properties @@ -49,7 +49,7 @@ log4j.appender.A1.File=${log.dir}/dspace.log # Set this to yyyy-MM-DD for daily log files, or yyyy-MM for monthly files log4j.appender.A1.DatePattern=yyyy-MM-dd # The number of log files to keep, or 0 to keep them all -log4j.appender.A1.MaxLogs=0 +log4j.appender.A1.MaxLogs=60 # A1 uses PatternLayout. log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=%d %-5p %c @ %m%n @@ -70,7 +70,7 @@ log4j.appender.A2.File=${log.dir}/checker.log # Set this to yyyy-MM-DD for daily log files, or yyyy-MM for monthly files log4j.appender.A2.DatePattern=yyyy-MM-dd # The number of log files to keep, or 0 to keep them all -log4j.appender.A2.MaxLogs=0 +log4j.appender.A2.MaxLogs=60 # A2 uses PatternLayout. log4j.appender.A2.layout=org.apache.log4j.PatternLayout log4j.appender.A2.layout.ConversionPattern=%m%n diff --git a/dspace/config/news-xmlui.xml b/dspace/config/news-xmlui.xml index a5985fb06d..b8b3f3f4bc 100644 --- a/dspace/config/news-xmlui.xml +++ b/dspace/config/news-xmlui.xml @@ -3,13 +3,13 @@
VTechWorks -

VTechWorks provides global access to Virginia Tech scholarship. VTechWorks includes journal articles, books, theses, dissertations, conference papers, slide presentations, technical reports, working papers, administrative documents, videos, images, and more by faculty, students, and staff. Write vtechworks@vt.edu to get help adding your content to VTechWorks. Read an article about VTechWorks or visit the Open@VT blog to learn about current VTechWorks activities.

+

VTechWorks provides global access to Virginia Tech scholarship, and is the repository for the university's open access policy. VTechWorks includes journal articles, books, theses, dissertations, conference papers, slide presentations, technical reports, working papers, administrative documents, videos, images, and more by faculty, students, and staff. Write vtechworks@vt.edu to get help adding your content to VTechWorks. Read an article about VTechWorks or visit the Open@VT blog to learn about current VTechWorks activities.

-

Want to publish a standalone dataset? Visit VTechData.

+

Want to publish a standalone dataset? Visit Virginia Tech Data Repository.

-

Faculty can deposit items to VTechWorks from Elements (EFARs). Visit the Provost's Elements page to learn more and to log in to Elements.

+

Faculty can deposit items to VTechWorks from Elements (EFARs). Visit the Provost's Elements page to learn more and to log in to Elements.

-

Want to see historical data on VTechWorks usage and content? See our spreadsheet of VTechWorks Stats and a map of global usage.

+

Want to see historical data on VTechWorks usage and content? See our spreadsheet of VTechWorks Stats and a map of global usage.

diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 9496744b7d..64456be33e 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -123,10 +123,15 @@ - + - + + + + + + @@ -245,10 +250,15 @@ - + - + + + + + + @@ -337,14 +347,16 @@ - - - - + + + + + + @@ -636,6 +648,15 @@ + + + + + dc.subject.lcc + + + + @@ -663,6 +684,15 @@ + + + + + dc.title.serial + + + + @@ -682,6 +712,25 @@ + + + + + dc.publisher + + + + + + + + + dc.rights + dc.rights.uri + + + + @@ -721,6 +770,15 @@ + + + + + dc.description.version + + + + diff --git a/dspace/modules/additions/src/main/java/org/dspace/administer/CommunityFiliator.java b/dspace/modules/additions/src/main/java/org/dspace/administer/CommunityFiliator.java new file mode 100644 index 0000000000..03bdccdb70 --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/administer/CommunityFiliator.java @@ -0,0 +1,292 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.PosixParser; +import org.apache.commons.collections.CollectionUtils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Community; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.CommunityService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; + +/** + * A command-line tool for setting/removing community/sub-community + * relationships. Takes community DB Id or handle arguments as inputs. + * + * @author rrodgers + * @version $Revision$ + */ + +public class CommunityFiliator +{ + + protected CommunityService communityService; + protected HandleService handleService; + + public CommunityFiliator() { + communityService = ContentServiceFactory.getInstance().getCommunityService(); + handleService = HandleServiceFactory.getInstance().getHandleService(); + } + + /** + * + * @param argv arguments + * @throws Exception if error + */ + public static void main(String[] argv) throws Exception + { + // create an options object and populate it + CommandLineParser parser = new PosixParser(); + + Options options = new Options(); + + options.addOption("s", "set", false, "set a parent/child relationship"); + options.addOption("r", "remove", false, + "remove a parent/child relationship"); + options.addOption("p", "parent", true, + "parent community (handle or database ID)"); + options.addOption("c", "child", true, + "child community (handle or databaseID)"); + options.addOption("h", "help", false, "help"); + + CommandLine line = parser.parse(options, argv); + + String command = null; // set or remove + String parentID = null; + String childID = null; + + if (line.hasOption('h')) + { + HelpFormatter myhelp = new HelpFormatter(); + myhelp.printHelp("CommunityFiliator\n", options); + System.out + .println("\nestablish a relationship: CommunityFiliator -s -p parentID -c childID"); + System.out + .println("remove a relationship: CommunityFiliator -r -p parentID -c childID"); + + System.exit(0); + } + + if (line.hasOption('s')) + { + command = "set"; + } + + if (line.hasOption('r')) + { + command = "remove"; + } + + if (line.hasOption('p')) // parent + { + parentID = line.getOptionValue('p'); + } + + if (line.hasOption('c')) // child + { + childID = line.getOptionValue('c'); + } + + // now validate + // must have a command set + if (command == null) + { + System.out + .println("Error - must run with either set or remove (run with -h flag for details)"); + System.exit(1); + } + + if ("set".equals(command) || "remove".equals(command)) + { + if (parentID == null) + { + System.out.println("Error - a parentID must be specified (run with -h flag for details)"); + System.exit(1); + } + + if (childID == null) + { + System.out.println("Error - a childID must be specified (run with -h flag for details)"); + System.exit(1); + } + } + + CommunityFiliator filiator = new CommunityFiliator(); + Context c = new Context(); + + // we are superuser! + c.turnOffAuthorisationSystem(); + + try + { + // validate and resolve the parent and child IDs into commmunities + Community parent = filiator.resolveCommunity(c, parentID); + Community child = filiator.resolveCommunity(c, childID); + + if (parent == null) + { + System.out.println("Error, parent community cannot be found: " + + parentID); + System.exit(1); + } + + if (child == null) + { + System.out.println("Error, child community cannot be found: " + + childID); + System.exit(1); + } + + if ("set".equals(command)) + { + filiator.filiate(c, parent, child); + } + else + { + filiator.defiliate(c, parent, child); + } + } + catch (SQLException sqlE) + { + System.out.println("Error - SQL exception: " + sqlE.toString()); + } + catch (AuthorizeException authE) + { + System.out.println("Error - Authorize exception: " + + authE.toString()); + } + catch (IOException ioE) + { + System.out.println("Error - IO exception: " + ioE.toString()); + } + } + + /** + * + * @param c context + * @param parent parent Community + * @param child child community + * @throws SQLException if database error + * @throws AuthorizeException if authorize error + * @throws IOException if IO error + */ + public void filiate(Context c, Community parent, Community child) + throws SQLException, AuthorizeException, IOException + { + // check that a valid filiation would be established + // first test - proposed child must currently be an orphan (i.e. + // top-level) + Community childDad = CollectionUtils.isNotEmpty(child.getParentCommunities()) ? child.getParentCommunities().iterator().next() : null; + + if (childDad != null) + { + System.out.println("Error, child community: " + child.getID() + + " already a child of: " + childDad.getID()); + System.exit(1); + } + + // second test - circularity: parent's parents can't include proposed + // child + List parentDads = parent.getParentCommunities(); + if (parentDads.contains(child)) + { + System.out.println( + "Error, circular parentage - child is parent of parent"); + System.exit(1); + } + + // everthing's OK + communityService.addSubcommunity(c, parent, child); + + // complete the pending transaction + c.complete(); + System.out.println("Filiation complete. Community: '" + parent.getID() + + "' is parent of community: '" + child.getID() + "'"); + } + + /** + * + * @param c context + * @param parent parent Community + * @param child child community + * @throws SQLException if database error + * @throws AuthorizeException if authorize error + * @throws IOException if IO error + */ + public void defiliate(Context c, Community parent, Community child) + throws SQLException, AuthorizeException, IOException + { + // verify that child is indeed a child of parent + List parentKids = parent.getSubcommunities(); + if (!parentKids.contains(child)) + { + System.out + .println("Error, child community not a child of parent community"); + System.exit(1); + } + + // OK remove the mappings - but leave the community, which will become + // top-level + child.removeParentCommunity(parent); + parent.removeSubCommunity(child); + communityService.update(c, child); + communityService.update(c, parent); + + // complete the pending transaction + c.complete(); + System.out.println("Defiliation complete. Community: '" + child.getID() + + "' is no longer a child of community: '" + parent.getID() + + "'"); + } + + /** + * Find a community by ID + * @param c context + * @param communityID community ID + * @return Community object + * @throws SQLException if database error + */ + protected Community resolveCommunity(Context c, String communityID) + throws SQLException + { + Community community = null; + + if (communityID.indexOf('/') != -1) + { + // has a / must be a handle + community = (Community) handleService.resolveToObject(c, + communityID); + + // ensure it's a community + if ((community == null) + || (community.getType() != Constants.COMMUNITY)) + { + community = null; + } + } + else + { + community = communityService.find(c, UUID.fromString(communityID)); + } + + return community; + } +} \ No newline at end of file diff --git a/dspace/modules/additions/src/main/java/org/dspace/browse/BrowseEngine.java b/dspace/modules/additions/src/main/java/org/dspace/browse/BrowseEngine.java new file mode 100644 index 0000000000..fe4738c0c7 --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/browse/BrowseEngine.java @@ -0,0 +1,804 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.browse; + +import org.apache.log4j.Logger; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.core.LogManager; +import org.dspace.sort.OrderFormat; +import org.dspace.sort.SortOption; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class does most of the actual grunt work of preparing a browse + * result. It takes in to a couple of available methods (depending on your + * desired browse type) a BrowserScope object, and uses this to produce a + * BrowseInfo object which is sufficient to describe to the User Interface + * the results of the requested browse + * + * @author Richard Jones + * + */ +public class BrowseEngine +{ + /** the logger for this class */ + private static final Logger log = Logger.getLogger(BrowseEngine.class); + + /** the browse scope which is the basis for our browse */ + private BrowserScope scope; + + /** the DSpace context */ + private final Context context; + + /** The Data Access Object for the browse tables */ + private final BrowseDAO dao; + + /** The Browse Index associated with the Browse Scope */ + private BrowseIndex browseIndex; + + /** + * Create a new instance of the Browse engine, using the given DSpace + * Context object. This will automatically assign a Data Access Object + * for the Browse Engine, based on the brand of the provided DBMS. + * + * @param context the DSpace context + * @throws BrowseException if browse error + */ + public BrowseEngine(Context context) + throws BrowseException + { + // set the context + this.context = context; + + // prepare the data access object + dao = BrowseDAOFactory.getInstance(context); + } + + /** + * Perform a standard browse, which will return a BrowseInfo + * object that represents the results for the current page, the + * total number of results, the range, and information to construct + * previous and next links on any web page + * + * @param bs the scope of the browse + * @return the results of the browse + * @throws BrowseException if browse error + */ + public BrowseInfo browse(BrowserScope bs) + throws BrowseException + { + log.debug(LogManager.getHeader(context, "browse", "")); + + // first, load the browse scope into the object + this.scope = bs; + + // since we use it so much, get the browse index out of the + // scope and store as a member + browseIndex = scope.getBrowseIndex(); + + // now make the decision as to how to browse + if (browseIndex.isMetadataIndex() && !scope.isSecondLevel()) + { + // this is a single value browse type that has not gone to + // the second level (i.e. authors, not items by a given author) + return browseByValue(scope); + } + else + { + // this is the full browse type or a browse that has gone to + // the second level + return browseByItem(scope); + } + } + + /** + * Perform a limited browse, which only returns the results requested, + * without any extraneous information. To perform a full browse, use + * BrowseEngine.browse() above. This supports Item browse only, and does + * not currently support focus or values. This method is used, for example, + * to generate the Recently Submitted Items results. + * + * @param bs the scope of the browse + * @return the results of the browse + * @throws BrowseException if browse error + */ + public BrowseInfo browseMini(BrowserScope bs) + throws BrowseException + { + log.info(LogManager.getHeader(context, "browse_mini", "")); + + // load the scope into the object + this.scope = bs; + + // since we use it so much, get the browse index out of the + // scope and store as a member + browseIndex = scope.getBrowseIndex(); + + // get the table name that we are going to be getting our data from + dao.setTable(browseIndex.getTableName()); + + // tell the browse query whether we are ascending or descending on the value + dao.setAscending(scope.isAscending()); + + // define a clause for the WHERE clause which will allow us to constrain + // our browse to a specified community or collection + if (scope.inCollection() || scope.inCommunity()) + { + if (scope.inCollection()) + { + Collection col = (Collection) scope.getBrowseContainer(); + dao.setContainerTable("collection2item"); + dao.setContainerIDField("collection_id"); + dao.setContainerID(col.getID()); + } + else if (scope.inCommunity()) + { + Community com = (Community) scope.getBrowseContainer(); + dao.setContainerTable("communities2item"); + dao.setContainerIDField("community_id"); + dao.setContainerID(com.getID()); + } + } + + dao.setOffset(scope.getOffset()); + dao.setLimit(scope.getResultsPerPage()); + + // assemble the ORDER BY clause + String orderBy = browseIndex.getSortField(scope.isSecondLevel()); + if (scope.getSortBy() > 0) + { + orderBy = "sort_" + Integer.toString(scope.getSortBy()); + } + dao.setOrderField(orderBy); + + // now run the query + List results = dao.doQuery(); + + // construct the mostly empty BrowseInfo object to pass back + BrowseInfo browseInfo = new BrowseInfo(results, 0, scope.getResultsPerPage(), 0); + + // add the browse index to the Browse Info + browseInfo.setBrowseIndex(browseIndex); + + // set the sort option for the Browse Info + browseInfo.setSortOption(scope.getSortOption()); + + // tell the Browse Info which way we are sorting + browseInfo.setAscending(scope.isAscending()); + + // tell the browse info what the container for the browse was + if (scope.inCollection() || scope.inCommunity()) + { + browseInfo.setBrowseContainer(scope.getBrowseContainer()); + } + + browseInfo.setResultsPerPage(scope.getResultsPerPage()); + + browseInfo.setEtAl(scope.getEtAl()); + + return browseInfo; + } + + /** + * Browse the archive by the full item browse mechanism. This produces a + * BrowseInfo object which contains full BrowseItem objects as its result + * set. + * + * @param bs the scope of the browse + * @return the results of the browse + * @throws BrowseException if browse error + */ + private BrowseInfo browseByItem(BrowserScope bs) + throws BrowseException + { + log.info(LogManager.getHeader(context, "browse_by_item", "")); + try + { + // get the table name that we are going to be getting our data from + dao.setTable(browseIndex.getTableName()); + + // tell the browse query whether we are ascending or descending on the value + dao.setAscending(scope.isAscending()); + + // assemble the value clause + String rawValue = null; + if (scope.hasFilterValue() && scope.isSecondLevel()) + { + String value = scope.getFilterValue(); + rawValue = value; + + // make sure the incoming value is normalised + value = OrderFormat.makeSortString(value, scope.getFilterValueLang(), + scope.getBrowseIndex().getDataType()); + + dao.setAuthorityValue(scope.getAuthorityValue()); + + // set the values in the Browse Query + if (scope.isSecondLevel()) + { + dao.setFilterValueField("value"); + dao.setFilterValue(rawValue); + } + else + { + dao.setFilterValueField("sort_value"); + dao.setFilterValue(value); + } + dao.setFilterValuePartial(scope.getFilterValuePartial()); + + // to apply the filtering, we need the distinct and map tables for the index + dao.setFilterMappingTables(browseIndex.getDistinctTableName(), + browseIndex.getMapTableName()); + } + + // define a clause for the WHERE clause which will allow us to constrain + // our browse to a specified community or collection + if (scope.inCollection() || scope.inCommunity()) + { + if (scope.inCollection()) + { + Collection col = (Collection) scope.getBrowseContainer(); + dao.setContainerTable("collection2item"); + dao.setContainerIDField("collection_id"); + dao.setContainerID(col.getID()); + } + else if (scope.inCommunity()) + { + Community com = (Community) scope.getBrowseContainer(); + dao.setContainerTable("communities2item"); + dao.setContainerIDField("community_id"); + dao.setContainerID(com.getID()); + } + } + + // this is the total number of results in answer to the query + int total = getTotalResults(); + + // assemble the ORDER BY clause + String orderBy = browseIndex.getSortField(scope.isSecondLevel()); + if (scope.getSortBy() > 0) + { + orderBy = "sort_" + Integer.toString(scope.getSortBy()); + } + dao.setOrderField(orderBy); + + int offset = scope.getOffset(); + String rawFocusValue = null; + if (offset < 1 && (scope.hasJumpToItem() || scope.hasJumpToValue() || scope.hasStartsWith())) + { + // We need to convert these to an offset for the actual browse query. + // First, get a value that we can look up in the ordering field + rawFocusValue = getJumpToValue(); + + // make sure the incoming value is normalised + String focusValue = normalizeJumpToValue(rawFocusValue); + + log.debug("browsing using focus: " + focusValue); + + // Convert the focus value into an offset + offset = getOffsetForValue(focusValue); + } + + dao.setOffset(offset); + + // assemble the LIMIT clause + dao.setLimit(scope.getResultsPerPage()); + + // Holder for the results + List results = null; + + // Does this browse have any contents? + if (total > 0) + { + // now run the query + results = dao.doQuery(); + + // now, if we don't have any results, we are at the end of the browse. This will + // be because a starts_with value has been supplied for which we don't have + // any items. + if (results.size() == 0) + { + // In this case, we will calculate a new offset for the last page of results + offset = total - scope.getResultsPerPage(); + if (offset < 0) + { + offset = 0; + } + + // And rerun the query + dao.setOffset(offset); + results = dao.doQuery(); + } + } + else + { + // No records, so make an empty list + results = new ArrayList<>(); + } + + // construct the BrowseInfo object to pass back +// BrowseInfo browseInfo = new BrowseInfo(results, position, total, offset); + BrowseInfo browseInfo = new BrowseInfo(results, offset, total, offset); + + if (offset + scope.getResultsPerPage() < total) + { + browseInfo.setNextOffset(offset + scope.getResultsPerPage()); + } + + if (offset - scope.getResultsPerPage() > -1) + { + browseInfo.setPrevOffset(offset - scope.getResultsPerPage()); + } + + // add the browse index to the Browse Info + browseInfo.setBrowseIndex(browseIndex); + + // set the sort option for the Browse Info + browseInfo.setSortOption(scope.getSortOption()); + + // tell the Browse Info which way we are sorting + browseInfo.setAscending(scope.isAscending()); + + // tell the Browse Info which level of browse we are at + browseInfo.setBrowseLevel(scope.getBrowseLevel()); + + // set the browse value if there is one + browseInfo.setValue(rawValue); + + // set the browse authority key if there is one + browseInfo.setAuthority(scope.getAuthorityValue()); + + // set the focus value if there is one + browseInfo.setFocus(rawFocusValue); + + if (scope.hasJumpToItem()) + { + browseInfo.setFocusItem(scope.getJumpToItem()); + } + + // tell the browse info if it is working from a starts with parameter + browseInfo.setStartsWith(scope.hasStartsWith()); + + // tell the browse info what the container for the browse was + if (scope.inCollection() || scope.inCommunity()) + { + browseInfo.setBrowseContainer(scope.getBrowseContainer()); + } + + browseInfo.setResultsPerPage(scope.getResultsPerPage()); + + browseInfo.setEtAl(scope.getEtAl()); + + return browseInfo; + } + catch (SQLException e) + { + log.error("caught exception: ", e); + throw new BrowseException(e); + } + } + + /** + * Browse the archive by single values (such as the name of an author). This + * produces a BrowseInfo object that contains Strings as the results of + * the browse + * + * @param bs the scope of the browse + * @return the results of the browse + * @throws BrowseException if browse error + */ + private BrowseInfo browseByValue(BrowserScope bs) + throws BrowseException + { + log.info(LogManager.getHeader(context, "browse_by_value", "focus=" + bs.getJumpToValue())); + + try + { + // get the table name that we are going to be getting our data from + // this is the distinct table constrained to either community or collection + dao.setTable(browseIndex.getDistinctTableName()); + dao.setStartsWith("0".equals(scope.getStartsWith()) && !scope.getOrder().equals("ASC") ? "9" : normalizeJumpToValue(scope.getStartsWith())); + // remind the DAO that this is a distinct value browse, so it knows what sort + // of query to build + dao.setDistinct(true); + + // tell the browse query whether we are ascending or descending on the value + dao.setAscending(scope.isAscending()); + + // inform dao about the display frequencies flag + dao.setEnableBrowseFrequencies(browseIndex.isDisplayFrequencies()); + + // if we want to display frequencies, we need to pass the map table + if (browseIndex.isDisplayFrequencies()){ + dao.setFilterMappingTables(null, browseIndex.getMapTableName()); + } + + // set our constraints on community or collection + if (scope.inCollection() || scope.inCommunity()) + { + // Scoped browsing of distinct metadata requires the mapping + // table to be specified. + if (!browseIndex.isDisplayFrequencies()) + dao.setFilterMappingTables(null, browseIndex.getMapTableName()); + + if (scope.inCollection()) + { + Collection col = (Collection) scope.getBrowseContainer(); + dao.setContainerTable("collection2item"); + dao.setContainerIDField("collection_id"); + dao.setContainerID(col.getID()); + } + else if (scope.inCommunity()) + { + Community com = (Community) scope.getBrowseContainer(); + dao.setContainerTable("communities2item"); + dao.setContainerIDField("community_id"); + dao.setContainerID(com.getID()); + } + } + + // this is the total number of results in answer to the query + int total = getTotalResults(true); + + // set the ordering field (there is only one option) + dao.setOrderField("sort_value"); + + // assemble the focus clause if we are to have one + // it will look like one of the following + // - sort_value < myvalue + // = sort_1 > myvalue + dao.setJumpToField("sort_value"); + int offset = scope.getOffset(); + int limit = scope.getResultsPerPage(); + List results = null; + + String rawFocusValue = null; + if (offset < 1 && scope.hasJumpToValue() || scope.hasStartsWith()) { + // store the value to tell the Browse Info object which value we are browsing on + rawFocusValue = getJumpToValue(); + } + if ("0".equals(scope.getStartsWith())) { + int currentSW = scope.getOrder().equals("ASC") ? 0 : 9; + results = new ArrayList(); + // While we haven't reached results or the end + while (results.size() < scope.getResultsPerPage() && currentSW <= 9 && currentSW >= 0) { + List thisNumResults = dao.doValueQuery(); + // If set contains our results + if (offset < thisNumResults.size()) { + // If we have found the rest, add and exit loop + if (offset + limit < thisNumResults.size()) { + results.addAll(thisNumResults.subList(offset, offset + limit)); + break; + } else { // Else add all we can and query again + thisNumResults = thisNumResults.subList(offset, thisNumResults.size()); + results.addAll(thisNumResults); + offset = 0; + limit -= thisNumResults.size(); + } + } else { + offset -= thisNumResults.size(); + } + dao.setStartsWith(String.valueOf(scope.getOrder().equals("ASC") ? ++currentSW : --currentSW)); + } + + offset = scope.getOffset(); + } else { + // assemble the offset and limit + dao.setOffset(offset); + dao.setLimit(limit); + + // Does this browse have any contents? + if (total > 0) { + // now run the query + results = dao.doValueQuery(); + + // now, if we don't have any results, we are at the end of the browse. This will + // be because a starts_with value has been supplied for which we don't have + // any items. + if (results.size() == 0) { + // In this case, we will calculate a new offset for the last page of results + offset = total - scope.getResultsPerPage(); + if (offset < 0) { + offset = 0; + } + + // And rerun the query + dao.setOffset(offset); + results = dao.doValueQuery(); + } + } else { + // No records, so make an empty list + results = new ArrayList(); + } + } + + // construct the BrowseInfo object to pass back + BrowseInfo browseInfo = new BrowseInfo(results, offset, total, offset); + + if (offset + scope.getResultsPerPage() < total) + { + browseInfo.setNextOffset(offset + scope.getResultsPerPage()); + } + + if (offset - scope.getResultsPerPage() > -1) + { + browseInfo.setPrevOffset(offset - scope.getResultsPerPage()); + } + + // add the browse index to the Browse Info + browseInfo.setBrowseIndex(browseIndex); + + // set the sort option for the Browse Info + browseInfo.setSortOption(scope.getSortOption()); + + // tell the Browse Info which way we are sorting + browseInfo.setAscending(scope.isAscending()); + + // tell the Browse Info which level of browse we are at + browseInfo.setBrowseLevel(scope.getBrowseLevel()); + + // set the browse value if there is one + browseInfo.setFocus(rawFocusValue); + + // tell the browse info if it is working from a starts with parameter + browseInfo.setStartsWith(scope.hasStartsWith()); + + // tell the browse info what the container for the browse was + if (scope.inCollection() || scope.inCommunity()) + { + browseInfo.setBrowseContainer(scope.getBrowseContainer()); + } + + browseInfo.setResultsPerPage(scope.getResultsPerPage()); + + return browseInfo; + } + catch (SQLException e) + { + log.error("caught exception: ", e); + throw new BrowseException(e); + } + } + + /** + * Return the focus value. + * + * @return the focus value to use + * @throws BrowseException if browse error + */ + private String getJumpToValue() + throws BrowseException + { + log.debug(LogManager.getHeader(context, "get_focus_value", "")); + + // if the focus is by value, just return it + if (scope.hasJumpToValue()) + { + log.debug(LogManager.getHeader(context, "get_focus_value_return", "return=" + scope.getJumpToValue())); + return scope.getJumpToValue(); + } + + // if the focus is to start with, then we need to return the value of the starts with + if (scope.hasStartsWith()) + { + log.debug(LogManager.getHeader(context, "get_focus_value_return", "return=" + scope.getStartsWith())); + return scope.getStartsWith(); + } + + // since the focus is not by value, we need to obtain it + + // get the id of the item to focus on + int id = scope.getJumpToItem(); + + // get the table name. We don't really need to care about whether we are in a + // community or collection at this point. This is only for full or second + // level browse, so there is no need to worry about distinct value browsing + String tableName = browseIndex.getTableName(); + + // we need to make sure that we select from the correct column. If the sort option + // is the 0th option then we use sort_value, but if it is one of the others we have + // to select from that column instead. Otherwise, we end up missing the focus value + // to do comparisons in other columns. The use of the focus value needs to be consistent + // across the browse + SortOption so = scope.getSortOption(); + if (so == null || so.getNumber() == 0) + { + if (browseIndex.getSortOption() != null) + { + so = browseIndex.getSortOption(); + } + } + + String col = "sort_1"; + if (so.getNumber() > 0) + { + col = "sort_" + Integer.toString(so.getNumber()); + } + + + // now get the DAO to do the query for us, returning the highest + // string value in the given column in the given table for the + // item (I think) + String max = dao.doMaxQuery(col, tableName, id); + + log.debug(LogManager.getHeader(context, "get_focus_value_return", "return=" + max)); + + return max; + } + + /** + * Convert the value into an offset into the table for this browse + * + * @param value value + * @return the focus value to use + * @throws BrowseException if browse error + */ + private int getOffsetForValue(String value) + throws BrowseException + { + // we need to make sure that we select from the correct column. If the sort option + // is the 0th option then we use sort_value, but if it is one of the others we have + // to select from that column instead. Otherwise, we end up missing the focus value + // to do comparisons in other columns. The use of the focus value needs to be consistent + // across the browse + SortOption so = scope.getSortOption(); + if (so == null || so.getNumber() == 0) + { + if (browseIndex.getSortOption() != null) + { + so = browseIndex.getSortOption(); + } + } + + String col = "sort_1"; + if (so.getNumber() > 0) + { + col = "sort_" + Integer.toString(so.getNumber()); + } + + // now get the DAO to do the query for us, returning the highest + // string value in the given column in the given table for the + // item (I think) + return dao.doOffsetQuery(col, value, scope.isAscending()); + } + + /** + * Convert the value into an offset into the table for this browse + * + * @param value value + * @return the focus value to use + * @throws BrowseException if browse error + */ + private int getOffsetForDistinctValue(String value) + throws BrowseException + { + if (!browseIndex.isMetadataIndex()) + { + throw new IllegalArgumentException("getOffsetForDistinctValue called when not a metadata index"); + } + + // now get the DAO to do the query for us, returning the highest + // string value in the given column in the given table for the + // item (I think) + return dao.doDistinctOffsetQuery("sort_value", value, scope.isAscending()); + } + + /** + * Return a normalized focus value. If there is no normalization that can be performed, + * return the focus value that is passed in. + * + * @param value a focus value to normalize + * @return the normalized focus value + * @throws BrowseException if browse error + */ + private String normalizeJumpToValue(String value) + throws BrowseException + { + // If the scope has a focus value (focus by value) + if (scope.hasJumpToValue()) + { + // Normalize it based on the specified language as appropriate for this index + return OrderFormat.makeSortString(scope.getJumpToValue(), scope.getJumpToValueLang(), scope.getBrowseIndex().getDataType()); + } + else if (scope.hasStartsWith()) + { + // Scope has a starts with, so normalize that instead + return OrderFormat.makeSortString(scope.getStartsWith(), null, scope.getBrowseIndex().getDataType()); + } + + // No focus value on the scope (ie. focus by id), so just return the passed focus value + // This is useful in cases where we have pulled a focus value from the index + // which will already be normalized, and avoids another DB lookup + return value; + } + + /** + * Get the total number of results for the browse. This is the same as + * calling getTotalResults(false) + * + * @return total + * @throws SQLException if database error + * @throws BrowseException if browse error + */ + private int getTotalResults() + throws SQLException, BrowseException + { + return getTotalResults(false); + } + + /** + * Get the total number of results. The argument determines whether this is a distinct + * browse or not as this has an impact on how results are counted + * + * @param distinct is this a distinct browse or not + * @return the total number of results available in this type of browse + * @throws SQLException if database error + * @throws BrowseException if browse error + */ + private int getTotalResults(boolean distinct) + throws SQLException, BrowseException + { + log.debug(LogManager.getHeader(context, "get_total_results", "distinct=" + distinct)); + + // tell the browse query whether we are distinct + dao.setDistinct(distinct); + + // ensure that the select is set to "*" + String[] select = { "*" }; + dao.setCountValues(select); + + // FIXME: it would be nice to have a good way of doing this in the DAO + // now reset all of the fields that we don't want to have constraining + // our count, storing them locally to reinstate later + String focusField = dao.getJumpToField(); + String focusValue = dao.getJumpToValue(); + String orderField = dao.getOrderField(); + int limit = dao.getLimit(); + int offset = dao.getOffset(); + + dao.setJumpToField(null); + dao.setJumpToValue(null); + dao.setOrderField(null); + dao.setLimit(-1); + dao.setOffset(-1); + + // perform the query and get the result + int count = dao.doCountQuery(); + if ("0".equals(dao.getStartsWith())) { + for (int x = 1; x <= 9; x++) { + dao.setStartsWith(String.valueOf(x)); + count += dao.doCountQuery(); + } + dao.setStartsWith("0"); + } else if ("9".equals(dao.getStartsWith())) { + for (int x = 8; x >= 0; x--) { + dao.setStartsWith(String.valueOf(x)); + count += dao.doCountQuery(); + } + dao.setStartsWith("9"); + } + + // now put back the values we removed for this method + dao.setJumpToField(focusField); + dao.setJumpToValue(focusValue); + dao.setOrderField(orderField); + dao.setLimit(limit); + dao.setOffset(offset); + dao.setCountValues(null); + + log.debug(LogManager.getHeader(context, "get_total_results_return", "return=" + count)); + + return count; + } +} \ No newline at end of file diff --git a/dspace/modules/additions/src/main/java/org/dspace/browse/BrowseInfo.java b/dspace/modules/additions/src/main/java/org/dspace/browse/BrowseInfo.java new file mode 100644 index 0000000000..88afe7d6cd --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/browse/BrowseInfo.java @@ -0,0 +1,927 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.browse; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.dspace.content.*; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.sort.SortOption; + +/** + * The results of a Browse, including all the contextual information about + * the query, as well as the results and associated information to create + * pageable navigation. + * + * @author Richard Jones + */ +public class BrowseInfo +{ + /** + * The results of the browse. + * FIXME: Unable to generify due to mixed usage + */ + private List results; + + /** + * The position of the first element of results within the Browse index. + * Positions begin with 0. + */ + private int overallPosition; + + /** + * The position of the requested object within the results. Offsets begin + * with 0. + */ + private int offset; + + /** + * The total number of items in the browse index. + */ + private int total; + + /** + * True if this browse was cached. + */ + private boolean cached; + + /** the browse index to which this pertains */ + private BrowseIndex browseIndex; + + /** the sort option being used */ + private SortOption sortOption; + + /** is the browse ascending or descending */ + private boolean ascending; + + /** what level of browse are we in? full and single front pages are 0, single value browse is 1 */ + private int level = 0; + + /** the value browsed upon */ + private String value; + + /** the authority key browsed upon */ + private String authority; + + /** is this a "starts_with" browse? */ + private boolean startsWith = false; + + /** Collection we are constrained to */ + private Collection collection; + + /** Community we are constrained to */ + private Community community; + + /** offset of the item at the top of the next page */ + private int nextOffset = -1; + + /** offset of the item at the top of the previous page */ + private int prevOffset = -1; + + /** the value upon which we are focusing */ + private String focus; + + /** number of results to display per page */ + private int resultsPerPage = -1; + + /** database id of the item upon which we are focusing */ + private int focusItem = -1; + + /** number of metadata elements to display before truncating using "et al" */ + private int etAl = -1; + + protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + + /** + * Constructor + * FIXME: Unable to generify due to mixed usage + * + * @param results + * A List of Browse results + * @param overallPosition + * The position of the first returned item in the overall index + * @param total + * The total number of items in the index + * @param offset + * The position of the requested item in the set of results + */ + public BrowseInfo(List results, int overallPosition, int total, int offset) + { + if (results == null) + { + throw new IllegalArgumentException("Null result list not allowed"); + } + + this.results = Collections.unmodifiableList(results); + this.overallPosition = overallPosition; + this.total = total; + this.offset = offset; + } + + /** + * @return the number of metadata fields at which to truncate with "et al" + */ + public int getEtAl() + { + return etAl; + } + + /** + * set the number of metadata fields at which to truncate with "et al" + * + * @param etAl + */ + public void setEtAl(int etAl) + { + this.etAl = etAl; + } + + /** + * @return Returns the focusItem. + */ + public int getFocusItem() + { + return focusItem; + } + + /** + * @param focusItem The focusItem to set. + */ + public void setFocusItem(int focusItem) + { + this.focusItem = focusItem; + } + + /** + * Does this browse have an item focus (as opposed to one of: no focus, + * a value focus) + * + * @return true if item focus, false if not + */ + public boolean hasItemFocus() + { + if (focusItem == -1) + { + return false; + } + return true; + } + + /** + * @return Returns the resultsPerPage. + */ + public int getResultsPerPage() + { + return resultsPerPage; + } + + /** + * @param resultsPerPage The resultsPerPage to set. + */ + public void setResultsPerPage(int resultsPerPage) + { + this.resultsPerPage = resultsPerPage; + } + + /** + * Is there a value associated with this browse + * + * @return true if a value, false if not + */ + public boolean hasValue() + { + if (this.value != null) + { + return true; + } + return false; + } + + /** + * Is there an authority key associated with this browse + * + * @return true if an authority key, false if not + */ + public boolean hasAuthority() + { + if (this.authority != null) + { + return true; + } + return false; + } + + /** + * Are there results for this browse, or was the result set empty? + * + * @return true if results, false if not + */ + public boolean hasResults() + { + if (results.size() > 0) + { + return true; + } + return false; + } + + /** + * @param focus the value to focus the browse around + */ + public void setFocus(String focus) + { + this.focus = focus; + } + + /** + * @return the value to focus the browse around + */ + public String getFocus() + { + return this.focus; + } + + /** + * Set the DSpaceObject that is the container for this browse. If this + * is not of type Collection or Community, this method will throw an + * exception + * + * @param dso the container object; a Community or Collection + * @throws BrowseException if browse error + */ + public void setBrowseContainer(DSpaceObject dso) + throws BrowseException + { + if (dso instanceof Collection) + { + this.collection = (Collection) dso; + } + else if (dso instanceof Community) + { + this.community = (Community) dso; + } + else + { + throw new BrowseException("The container must be a community or a collection"); + } + } + + /** + * Obtain a DSpaceObject that represents the container object. This will be + * a Community or a Collection + * + * @return A DSpaceObject representing a Community or a Collection + */ + public DSpaceObject getBrowseContainer() + { + if (this.collection != null) + { + return this.collection; + } + if (this.community != null) + { + return this.community; + } + return null; + } + + /** + * @param level the browse level + */ + public void setBrowseLevel(int level) + { + this.level = level; + } + + /** + * @return the browse level + */ + public int getBrowseLevel() + { + return this.level; + } + + /** + * @param offset the database id of the item at the top of the next page + */ + public void setNextOffset(int offset) + { + this.nextOffset = offset; + } + + /** + * @return the database id of the item at the top of the next page + */ + public int getNextOffset() + { + return this.nextOffset; + } + + /** + * @return Returns the ascending. + */ + public boolean isAscending() + { + return ascending; + } + + /** + * @param ascending The ascending to set. + */ + public void setAscending(boolean ascending) + { + this.ascending = ascending; + } + + /** + * @return Returns the browseIndex. + */ + public BrowseIndex getBrowseIndex() + { + return browseIndex; + } + + /** + * @param browseIndex The browseIndex to set. + */ + public void setBrowseIndex(BrowseIndex browseIndex) + { + this.browseIndex = browseIndex; + } + + /** + * @return Returns the prevItem. + */ + public int getPrevOffset() + { + return prevOffset > -1 ? prevOffset : 0; + } + + /** + * @param prevOffset The prevOffset to set. + */ + public void setPrevOffset(int prevOffset) + { + this.prevOffset = prevOffset; + } + + /** + * @return Returns the sortOption. + */ + public SortOption getSortOption() + { + return sortOption; + } + + /** + * @param sortOption The sortOption to set. + */ + public void setSortOption(SortOption sortOption) + { + this.sortOption = sortOption; + } + + /** + * @return Returns the startsWith. + */ + public boolean isStartsWith() + { + return startsWith; + } + + /** + * @param startsWith The startsWith to set. + */ + public void setStartsWith(boolean startsWith) + { + this.startsWith = startsWith; + } + + /** + * @return Returns the value. + */ + public String getValue() + { + return value; + } + + /** + * @param value The value to set. + */ + public void setValue(String value) + { + this.value = value; + } + + /** + * @return Returns the authority key. + */ + public String getAuthority() + { + return authority; + } + + /** + * @param authority The authority key to set. + */ + public void setAuthority(String authority) + { + this.authority = authority; + } + + /** + * is this a top level (0) browse? Examples of this are a full item + * browse or a single browse. Other browse types are considered + * second level (1) + * + * @return true if top level, false if not + */ + public boolean isTopLevel() + { + if (this.level == 0) + { + return true; + } + return false; + } + + /** + * Is this a second level (1) browse? Examples of this are a single + * value browse (e.g. all items by a given author) + * + * @return true if second level, false if not + */ + public boolean isSecondLevel() + { + if (this.level == 1) + { + return true; + } + return false; + } + + /** + * The results of the Browse. Each member of the list is either a String array + * (for the authors browse: first element the value, second element the authority key) + * or an {@link org.dspace.content.Item}(for the + * other browses). + * + * @return Result list. This list cannot be modified. + */ + public List getResults() + { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + /** + * Return the results of the Browse as an array of String array. + * The first element (i.e. index 0) is the value, the second is the authority key + * + * @return The results of the Browse as a String array. + */ + public String[][] getStringResults() + { + return (String[][]) results.toArray(new String[results.size()][2]); + } + + /** + * @deprecated + * @return an empty array of Item. + */ + public Item[] getItemResults() + { + return new Item[0]; + } + + /** + * Return the results of the Browse as a BrowseItem array + * + * @return the results of the browse as a BrowseItem array + */ + public List getBrowseItemResults() + { + return results; + } + + /** + * Return the number of results. + * + * @return The number of results. + */ + public int getResultCount() + { + return results.size(); + } + + /** + * Return the position of the results in index being browsed. This is 0 for + * the start of the index. + * + * @return The position of the results in index being browsed. + */ + public int getOverallPosition() + { + return overallPosition; + } + + /** + * Return the total number of items in the index. + * + * @return The total number of items in the index. + */ + public int getTotal() + { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + /** + * Return the position of the requested item or value in the set of results. + * + * @return The position of the requested item or value in the set of results + */ + public int getOffset() + { + return offset; + } + + /** + * True if there are no previous results from the browse. + * + * @return True if there are no previous results from the browse + */ + public boolean isFirst() + { + return overallPosition == 0; + } + + /** + * True if these are the last results from the browse. + * + * @return True if these are the last results from the browse + */ + public boolean isLast() + { + return (overallPosition + getResultCount()) == total; + } + + /** + * True if this browse was cached. + * @return true/false + */ + public boolean wasCached() + { + return cached; + } + + /** + * Set whether this browse was cached. + */ + void setCached(boolean cached) + { + this.cached = cached; + } + + /** + * are we browsing within a Community container? + * + * @return true if in community, false if not + */ + public boolean inCommunity() + { + if (this.community != null) + { + return true; + } + return false; + } + + /** + * are we browsing within a Collection container + * + * @return true if in collection, false if not + */ + public boolean inCollection() + { + if (this.collection != null) + { + return true; + } + return false; + } + + /** + * Are there further results for the browse that haven't been returned yet? + * + * @return true if next page, false if not + */ + public boolean hasNextPage() + { + if (nextOffset > -1) + { + return true; + } + return false; + } + + /** + * Are there results prior to these that haven't been returned here? + * + * @return true if previous page, false if not + */ + public boolean hasPrevPage() + { + if (offset > 0) + { + return true; + } + return false; + } + + /** + * Does this browse have a focus? + * + * @return true if focus, false if not + */ + public boolean hasFocus() + { + if ("".equals(focus) || focus == null) + { + return false; + } + return true; + } + + /** + * Get an integer representing the number within the total set of results which + * marks the position of the first result in the current sub-set + * + * @return the start point of the browse page + */ + public int getStart() + { + return overallPosition + 1; + } + + /** + * Get an integer representing the number within the total set of results which + * marks the position of the last result in the current sub-set + * + * @return the end point of the browse page + */ + public int getFinish() + { + return overallPosition + results.size(); + } + + /** + * Utility method for obtaining a string representation of the browse. This is + * useful only for debug + * @return String representation + */ + public String toString() + { + try + { + StringBuffer sb = new StringBuffer(); + + // calculate the range for display + String from = Integer.toString(overallPosition + 1); + String to = Integer.toString(overallPosition + results.size()); + String of = Integer.toString(total); + + // report on the positional information of the browse + sb.append("BrowseInfo String Representation: "); + sb.append("Browsing " + from + " to " + to + " of " + of + " "); + + // insert the information about which index + sb.append("in index: " + browseIndex.getName() + + " (data type: " + browseIndex.getDataType() + + ", display type: " + browseIndex.getDisplayType() + ") "); + + sb.append("||"); + + // report on the browse scope container + String container = "all of DSpace"; + DSpaceObject theContainer = null; + if (inCollection()) + { + container = "collection"; + theContainer = this.collection; + } + else if (inCommunity()) + { + container = "community"; + theContainer = this.community; + } + + String containerID = "no id available/necessary"; + if (theContainer != null) + { + containerID = theContainer.getID().toString() + " (" + theContainer.getHandle() + ")"; + } + + sb.append("Browsing in " + container + ": " + containerID); + sb.append("||"); + + // load the item list display configuration + ItemListConfig config = new ItemListConfig(); + + // some information about the columns to be displayed + if (browseIndex.isItemIndex()) + { + sb.append("Listing over " + Integer.toString(config.numCols()) + " columns: "); + for (int k = 1; k <= config.numCols(); k++) + { + if (k > 1) + { + sb.append(","); + } + String[] meta = config.getMetadata(k); + sb.append(meta[0] + "." + meta[1] + "." + meta[2]); + } + + if (value != null) + { + sb.append(" on value: ").append(value); + } + + if (isStartsWith()) + { + sb.append(" sort column starting with: ").append(focus); + } + else if (hasFocus()) + { + sb.append(" sort column focus: ").append(focus); + } + } + else if (browseIndex.isMetadataIndex()) + { + sb.append("Listing single column: ").append(browseIndex.getMetadata()); + if (isStartsWith()) + { + sb.append(" sort column starting with: ").append(focus); + } + else if (hasFocus()) + { + sb.append(" sort column focus: ").append(focus); + } + } + + sb.append("||"); + + // some information about how the data is sorted + String direction = (ascending ? "ASC" : "DESC"); + sb.append("Sorting by: " + sortOption.getMetadata() + " " + direction + + " (option " + Integer.toString(sortOption.getNumber()) + ")"); + sb.append("||"); + + // output the results + if (browseIndex.isMetadataIndex() && !isSecondLevel()) + { + sb.append(valueListingString()); + } + else if (browseIndex.isItemIndex() || isSecondLevel()) + { + sb.append(fullListingString(config)); + } + + sb.append("||"); + + // tell us what the next and previous values are going to be + sb.append("Top of next page: "); + if (hasNextPage()) + { + sb.append("offset: ").append(Integer.toString(this.nextOffset)); + } + else + { + sb.append("n/a"); + } + sb.append(";"); + + sb.append("Top of previous page: "); + if (hasPrevPage()) + { + sb.append("offset: ").append(Integer.toString(this.prevOffset)); + } + else + { + sb.append("n/a"); + } + + sb.append("||"); + + return sb.toString(); + } + catch (SQLException e) + { + return e.getMessage(); + } + catch (BrowseException e) + { + return e.getMessage(); + } + } + + /** + * A utility method for generating a string to represent a single item's + * entry in the browse + * + * @param config + * @return + * @throws SQLException if database error + */ + private String fullListingString(ItemListConfig config) + throws SQLException + { + // report on all the results contained herein + StringBuffer sb = new StringBuffer(); + + Iterator itr = results.iterator(); + while (itr.hasNext()) + { + Item bi = (Item) itr.next(); + if (bi == null) + { + sb.append("{{ NULL ITEM }}"); + break; + } + sb.append("{{Item ID: " + bi.getID().toString() + " :: "); + + for (int j = 1; j <= config.numCols(); j++) + { + String[] md = config.getMetadata(j); + if (md == null) + { + sb.append("{{ NULL METADATA }}"); + break; + } + List values = itemService.getMetadata(bi, md[0], md[1], md[2], Item.ANY); + StringBuffer value = new StringBuffer(); + if (values != null) + { + for (int i = 0; i < values.size(); i++) + { + if (i > 0) + { + value.append(","); + } + value.append(values.get(i).getValue()); + } + } + else + { + value.append("-"); + } + String metadata = "[" + md[0] + "." + md[1] + "." + md[2] + ":" + value.toString() + "]"; + sb.append(metadata); + } + + sb.append("}}"); + } + + return sb.toString(); + } + + /** + * A utility method for representing a single value in the browse + * + * @return + */ + private String valueListingString() + { + // report on all the results contained herein + StringBuffer sb = new StringBuffer(); + + Iterator itr = results.iterator(); + while (itr.hasNext()) + { + String theValue = (String) itr.next(); + if (theValue == null) + { + sb.append("{{ NULL VALUE }}"); + break; + } + sb.append("{{Value: " + theValue + "}}"); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/dspace/modules/additions/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace/modules/additions/src/main/java/org/dspace/browse/SolrBrowseDAO.java new file mode 100644 index 0000000000..082770c8f6 --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -0,0 +1,837 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.browse; + +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.discovery.*; +import org.dspace.discovery.DiscoverQuery.SORT_ORDER; +import org.dspace.discovery.DiscoverResult.FacetResult; +import org.dspace.discovery.DiscoverResult.SearchDocument; +import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; +import org.dspace.services.factory.DSpaceServicesFactory; + +import java.io.Serializable; +import java.sql.SQLException; +import java.util.*; + +/** + * + * @author Andrea Bollini (CILEA) + * @author Adán Román Ruiz at arvo.es (bugfix) + * @author Panagiotis Koutsourakis (National Documentation Centre) (bugfix) + * @author Kostas Stamatis (National Documentation Centre) (bugfix) + * + */ +public class SolrBrowseDAO implements BrowseDAO +{ + public SolrBrowseDAO(Context context) + { + this.context = context; + } + + static private class FacetValueComparator + implements Comparator, Serializable + { + @Override + public int compare(Object o1, Object o2) + { + String s1 = "", s2 = ""; + if (o1 instanceof FacetResult && o2 instanceof String) + { + FacetResult c = (FacetResult) o1; + s1 = c.getSortValue(); + s2 = (String) o2; + } + else if (o2 instanceof FacetResult && o1 instanceof String) + { + FacetResult c = (FacetResult) o2; + s1 = (String) o1; + s2 = c.getSortValue(); + } + // both object are FacetResult so they are already sorted + return s1.compareTo(s2); + } + } + + /** Log4j log */ + private static final Logger log = Logger.getLogger(SolrBrowseDAO.class); + + /** The DSpace context */ + private final Context context; + + // SQL query related attributes for this class + + /** table(s) to select from */ + private String table = null; + + /** field to look for focus value in */ + private String focusField = null; + + /** value to start browse from in focus field */ + private String focusValue = null; + + private String startsWith = null; + + /** field to look for value in */ + private String valueField = null; + + /** value to restrict browse to (e.g. author name) */ + private String value = null; + + private String authority = null; + + /** exact or partial matching of the value */ + private boolean valuePartial = false; + + /** the table that defines the mapping for the relevant container */ + private String containerTable = null; + + /** + * the name of the field which contains the container id (e.g. + * collection_id) + */ + private String containerIDField = null; + + /** the database id of the container we are constraining to */ + private UUID containerID = null; + + /** the column that we are sorting results by */ + private String orderField = null; + + /** whether to sort results ascending or descending */ + private boolean ascending = true; + + /** the limit of number of results to return */ + private int limit = -1; + + /** the offset of the start point */ + private int offset = 0; + + /** whether to use the equals comparator in value comparisons */ + private boolean equalsComparator = true; + + /** whether this is a distinct browse or not */ + private boolean distinct = false; + + private String facetField; + + protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + + // administrative attributes for this class + + + SearchService searcher = DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + SearchService.class.getName(), SearchService.class); + + private DiscoverResult sResponse = null; + + private boolean itemsWithdrawn = false; + private boolean itemsDiscoverable = true; + + private boolean showFrequencies; + + private DiscoverResult getSolrResponse() throws BrowseException + { + if (sResponse == null) + { + DiscoverQuery query = new DiscoverQuery(); + addLocationScopeFilter(query); + addStatusFilter(query); + if (distinct) + { + DiscoverFacetField dff; + if (StringUtils.isNotBlank(startsWith)) { + dff = new DiscoverFacetField(facetField, + DiscoveryConfigurationParameters.TYPE_TEXT, -1, + DiscoveryConfigurationParameters.SORT.VALUE, startsWith); + } else { + dff = new DiscoverFacetField(facetField, + DiscoveryConfigurationParameters.TYPE_TEXT, -1, + DiscoveryConfigurationParameters.SORT.VALUE); + } + query.addFacetField(dff); + query.setFacetMinCount(1); + query.setMaxResults(0); + } + else + { + query.setMaxResults(limit/* > 0 ? limit : 20*/); + if (offset > 0) + { + query.setStart(offset); + } + + // caution check first authority, value is always present! + if (authority != null) + { + query.addFilterQueries("{!field f="+facetField + "_authority_filter}" + + authority); + } + else if (value != null && !valuePartial) + { + query.addFilterQueries("{!field f="+facetField + "_value_filter}" + value); + } + else if (valuePartial) + { + query.addFilterQueries("{!field f="+facetField + "_partial}" + value); + } + // filter on item to be sure to don't include any other object + // indexed in the Discovery Search core + query.addFilterQueries("search.resourcetype:" + Constants.ITEM); + if (orderField != null) + { + query.setSortField("bi_" + orderField + "_sort", + ascending ? SORT_ORDER.asc : SORT_ORDER.desc); + } + } + try + { + sResponse = searcher.search(context, query, itemsWithdrawn + || !itemsDiscoverable); + } + catch (SearchServiceException e) + { + throw new BrowseException(e); + } + } + return sResponse; + } + + private void addStatusFilter(DiscoverQuery query) + { + if (itemsWithdrawn) + { + query.addFilterQueries("withdrawn:true"); + } + else if (!itemsDiscoverable) + { + query.addFilterQueries("discoverable:false"); + // TODO + + try + { + if (!authorizeService.isAdmin(context) + && (authorizeService.isCommunityAdmin(context) + || authorizeService.isCollectionAdmin(context))) + { + query.addFilterQueries(searcher.createLocationQueryForAdministrableItems(context)); + } + } + catch (SQLException ex) + { + log.error(ex); + } + } + } + + private void addLocationScopeFilter(DiscoverQuery query) + { + if (containerID != null) + { + if (containerIDField.startsWith("collection")) + { + query.addFilterQueries("location.coll:" + containerID); + } + else if (containerIDField.startsWith("community")) + { + query.addFilterQueries("location.comm:" + containerID); + } + } + } + + @Override + public int doCountQuery() throws BrowseException + { + DiscoverResult resp = getSolrResponse(); + int count = 0; + if (distinct) + { + List facetResults = resp.getFacetResult(facetField); + count = facetResults.size(); + } + else + { + // we need to cast to int to respect the BrowseDAO contract... + count = (int) resp.getTotalSearchResults(); + // FIXME null the response cache + // the BrowseEngine send fake argument to the BrowseDAO for the + // count... + sResponse = null; + } + return count; + } + + @Override + public List doValueQuery() throws BrowseException + { + DiscoverResult resp = getSolrResponse(); + List facet = resp.getFacetResult(facetField); + int count = doCountQuery(); + int start = offset > 0 ? offset : 0; + int max = limit > 0 ? limit : count; //if negative, return everything + List result = new ArrayList<>(); + if (ascending) + { + for (int i = start; i < (start + max) && i < count; i++) + { + FacetResult c = facet.get(i); + String freq = showFrequencies ? String.valueOf(c.getCount()) + : ""; + result.add(new String[] { c.getDisplayedValue(), + c.getAuthorityKey(), freq }); + } + } + else + { + for (int i = count - start - 1; i >= count - (start + max) + && i >= 0; i--) + { + FacetResult c = facet.get(i); + String freq = showFrequencies ? String.valueOf(c.getCount()) + : ""; + result.add(new String[] { c.getDisplayedValue(), + c.getAuthorityKey(), freq }); + } + } + + return result; + } + + @Override + public List doQuery() throws BrowseException + { + DiscoverResult resp = getSolrResponse(); + + List bitems = new ArrayList<>(); + for (DSpaceObject solrDoc : resp.getDspaceObjects()) + { + // FIXME introduce project, don't retrieve Item immediately when + // processing the query... + Item item = (Item) solrDoc; + bitems.add(item); + } + return bitems; + } + + @Override + public String doMaxQuery(String column, String table, int itemID) + throws BrowseException + { + DiscoverQuery query = new DiscoverQuery(); + query.setQuery("search.resourceid:" + itemID + + " AND search.resourcetype:" + Constants.ITEM); + query.setMaxResults(1); + DiscoverResult resp = null; + try + { + resp = searcher.search(context, query); + } + catch (SearchServiceException e) + { + throw new BrowseException(e); + } + if (resp.getTotalSearchResults() > 0) + { + SearchDocument doc = resp.getSearchDocument( + resp.getDspaceObjects().get(0)).get(0); + return (String) doc.getSearchFieldValues(column).get(0); + } + return null; + } + + @Override + public int doOffsetQuery(String column, String value, boolean isAscending) + throws BrowseException + { + DiscoverQuery query = new DiscoverQuery(); + addLocationScopeFilter(query); + addStatusFilter(query); + query.setMaxResults(0); + query.addFilterQueries("search.resourcetype:" + Constants.ITEM); + + // We need to take into account the fact that we may be in a subset of the items + if (authority != null) + { + query.addFilterQueries("{!field f="+facetField + "_authority_filter}" + + authority); + } + else if (this.value != null && !valuePartial) + { + query.addFilterQueries("{!field f="+facetField + "_value_filter}" + this.value); + } + else if (valuePartial) + { + query.addFilterQueries("{!field f="+facetField + "_partial}" + this.value); + } + + if (isAscending) + { + query.setQuery("bi_"+column + "_sort" + ": [* TO \"" + value + "\"}"); + } + else + { + query.setQuery("bi_" + column + "_sort" + ": {\"" + value + "\" TO *]"); + query.addFilterQueries("-(bi_" + column + "_sort" + ":" + value + "*)"); + } + boolean includeUnDiscoverable = itemsWithdrawn || !itemsDiscoverable; + DiscoverResult resp = null; + try + { + resp = searcher.search(context, query, includeUnDiscoverable); + } + catch (SearchServiceException e) + { + throw new BrowseException(e); + } + return (int) resp.getTotalSearchResults(); + } + + @Override + public int doDistinctOffsetQuery(String column, String value, + boolean isAscending) throws BrowseException + { + DiscoverResult resp = getSolrResponse(); + List facets = resp.getFacetResult(facetField); + Comparator comparator = new SolrBrowseDAO.FacetValueComparator(); + Collections.sort(facets, comparator); + int x = Collections.binarySearch(facets, value, comparator); + int ascValue = (x >= 0) ? x : -(x + 1); + if (isAscending) + { + return ascValue; + } + else + { + return doCountQuery() - ascValue; + } + } + + @Override + public boolean isEnableBrowseFrequencies() + { + return showFrequencies; + } + + @Override + public void setEnableBrowseFrequencies(boolean enableBrowseFrequencies) + { + showFrequencies = enableBrowseFrequencies; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getContainerID() + */ + @Override + public UUID getContainerID() + { + return containerID; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getContainerIDField() + */ + @Override + public String getContainerIDField() + { + return containerIDField; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getContainerTable() + */ + @Override + public String getContainerTable() + { + return containerTable; + } + + // FIXME is this in use? + @Override + public String[] getCountValues() + { + return null; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getFocusField() + */ + @Override + public String getJumpToField() + { + return focusField; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getFocusValue() + */ + @Override + public String getJumpToValue() + { + return focusValue; + } + + @Override + public void setStartsWith(String startsWith) { + sResponse = null; + this.startsWith = startsWith; + } + + @Override + public String getStartsWith() { + return startsWith; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getLimit() + */ + @Override + public int getLimit() + { + return limit; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getOffset() + */ + @Override + public int getOffset() + { + return offset; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getOrderField() + */ + @Override + public String getOrderField() + { + return orderField; + } + + // is this in use? + @Override + public String[] getSelectValues() + { + return null; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getTable() + */ + @Override + public String getTable() + { + return table; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getValue() + */ + @Override + public String getFilterValue() + { + return value; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#getValueField() + */ + @Override + public String getFilterValueField() + { + return valueField; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#isAscending() + */ + @Override + public boolean isAscending() + { + return ascending; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#isDistinct() + */ + @Override + public boolean isDistinct() + { + return this.distinct; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setAscending(boolean) + */ + @Override + public void setAscending(boolean ascending) + { + this.ascending = ascending; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setContainerID(int) + */ + @Override + public void setContainerID(UUID containerID) + { + this.containerID = containerID; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setContainerIDField(java.lang.String) + */ + @Override + public void setContainerIDField(String containerIDField) + { + this.containerIDField = containerIDField; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setContainerTable(java.lang.String) + */ + @Override + public void setContainerTable(String containerTable) + { + this.containerTable = containerTable; + + } + + // is this in use? + @Override + public void setCountValues(String[] fields) + { + // this.countValues = fields; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setDistinct(boolean) + */ + @Override + public void setDistinct(boolean bool) + { + this.distinct = bool; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setEqualsComparator(boolean) + */ + @Override + public void setEqualsComparator(boolean equalsComparator) + { + this.equalsComparator = equalsComparator; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setFocusField(java.lang.String) + */ + @Override + public void setJumpToField(String focusField) + { + this.focusField = focusField; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setFocusValue(java.lang.String) + */ + @Override + public void setJumpToValue(String focusValue) + { + this.focusValue = focusValue; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setLimit(int) + */ + @Override + public void setLimit(int limit) + { + this.limit = limit; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setOffset(int) + */ + @Override + public void setOffset(int offset) + { + this.offset = offset; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setOrderField(java.lang.String) + */ + @Override + public void setOrderField(String orderField) + { + this.orderField = orderField; + + } + + // is this in use? + @Override + public void setSelectValues(String[] selectValues) + { + // this.selectValues = selectValues; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setTable(java.lang.String) + */ + @Override + public void setTable(String table) + { + if (table.equals(BrowseIndex.getWithdrawnBrowseIndex().getTableName())) + { + itemsWithdrawn = true; + } + else if (table.equals(BrowseIndex.getPrivateBrowseIndex().getTableName())) + { + itemsDiscoverable = false; + } + facetField = table; + } + + @Override + public void setFilterMappingTables(String tableDis, String tableMap) + { + if (tableDis != null) + { + this.facetField = tableDis; + } + // this.fields = tableDis; + // this.tableMap = tableMap; + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setValue(java.lang.String) + */ + @Override + public void setFilterValue(String value) + { + this.value = value; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setFilterValuePartial(boolean) + */ + @Override + public void setFilterValuePartial(boolean part) + { + this.valuePartial = part; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#setValueField(java.lang.String) + */ + @Override + public void setFilterValueField(String valueField) + { + this.valueField = valueField; + + } + + /* + * (non-Javadoc) + * + * @see org.dspace.browse.BrowseDAO#useEqualsComparator() + */ + @Override + public boolean useEqualsComparator() + { + return equalsComparator; + } + + @Override + public String getAuthorityValue() + { + return authority; + } + + @Override + public void setAuthorityValue(String value) + { + this.authority = value; + } +} diff --git a/dspace/modules/additions/src/main/java/org/dspace/content/Community.java b/dspace/modules/additions/src/main/java/org/dspace/content/Community.java new file mode 100644 index 0000000000..d8626dd082 --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/content/Community.java @@ -0,0 +1,274 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content; + +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.log4j.Logger; +import org.dspace.content.comparator.NameAscendingComparator; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.CommunityService; +import org.dspace.core.*; +import org.dspace.eperson.Group; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.proxy.HibernateProxyHelper; + +import javax.persistence.*; +import java.util.*; + +/** + * Class representing a community + *

+ * The community's metadata (name, introductory text etc.) is loaded into' + * memory. Changes to this metadata are only reflected in the database after + * update is called. + * + * @author Robert Tansley + * @version $Revision$ + */ +@Entity +@Table(name="community") +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, include = "non-lazy") +public class Community extends DSpaceObject implements DSpaceObjectLegacySupport +{ + /** log4j category */ + private static final Logger log = Logger.getLogger(Community.class); + + @Column(name="community_id", insertable = false, updatable = false) + private Integer legacyId; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "community2community", + joinColumns = {@JoinColumn(name = "parent_comm_id") }, + inverseJoinColumns = {@JoinColumn(name = "child_comm_id") } + ) + private Set subCommunities = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "subCommunities") + private Set parentCommunities = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "communities", cascade = {CascadeType.PERSIST}) + private Set collections = new HashSet<>(); + + @OneToOne + @JoinColumn(name = "admin") + /** The default group of administrators */ + private Group admins; + + /** The logo bitstream */ + @OneToOne + @JoinColumn(name = "logo_bitstream_id") + private Bitstream logo = null; + + // Keys for accessing Community metadata + public static final String COPYRIGHT_TEXT = "copyright_text"; + public static final String INTRODUCTORY_TEXT = "introductory_text"; + public static final String SHORT_DESCRIPTION = "short_description"; + public static final String SIDEBAR_TEXT = "side_bar_text"; + + @Transient + protected transient CommunityService communityService; + + /** + * Protected constructor, create object using: + * {@link org.dspace.content.service.CommunityService#create(Community, Context)} + * or + * {@link org.dspace.content.service.CommunityService#create(Community, Context, String)} + * + */ + protected Community() + { + + } + + void addSubCommunity(Community subCommunity) + { + subCommunities.add(subCommunity); + setModified(); + } + + public void removeSubCommunity(Community subCommunity) + { + subCommunities.remove(subCommunity); + setModified(); + } + + /** + * Get the logo for the community. null is return if the + * community does not have a logo. + * + * @return the logo of the community, or null + */ + public Bitstream getLogo() + { + return logo; + } + + void setLogo(Bitstream logo) { + this.logo = logo; + setModified(); + } + + /** + * Get the default group of administrators, if there is one. Note that the + * authorization system may allow others to be administrators for the + * community. + *

+ * The default group of administrators for community 100 is the one called + * community_100_admin. + * + * @return group of administrators, or null if there is no + * default group. + */ + public Group getAdministrators() + { + return admins; + } + + void setAdmins(Group admins) { + this.admins = admins; + setModified(); + } + + /** + * Get the collections in this community. Throws an SQLException because + * creating a community object won't load in all collections. + * + * @return array of Collection objects + */ + public List getCollections() + { + // We return a copy because we do not want people to add elements to this collection directly. + // We return a list to maintain backwards compatibility + Collection[] output = collections.toArray(new Collection[]{}); + Arrays.sort(output, new NameAscendingComparator()); + return Arrays.asList(output); + } + + void addCollection(Collection collection) + { + collections.add(collection); + } + + void removeCollection(Collection collection) + { + collections.remove(collection); + } + + /** + * Get the immediate sub-communities of this community. Throws an + * SQLException because creating a community object won't load in all + * collections. + * + * @return array of Community objects + */ + public List getSubcommunities() + { + // We return a copy because we do not want people to add elements to this collection directly. + // We return a list to maintain backwards compatibility + Community[] output = subCommunities.toArray(new Community[]{}); + Arrays.sort(output, new NameAscendingComparator()); + return Arrays.asList(output); + } + + /** + * Return the parent community of this community, or null if the community + * is top-level + * + * @return the immediate parent community, or null if top-level + */ + public List getParentCommunities() + { + // We return a copy because we do not want people to add elements to this collection directly. + // We return a list to maintain backwards compatibility + Community[] output = parentCommunities.toArray(new Community[]{}); + Arrays.sort(output, new NameAscendingComparator()); + return Arrays.asList(output); + } + + void addParentCommunity(Community parentCommunity) { + parentCommunities.add(parentCommunity); + } + + void clearParentCommunities(){ + parentCommunities.clear(); + } + + public void removeParentCommunity(Community parentCommunity) + { + parentCommunities.remove(parentCommunity); + setModified(); + } + + /** + * Return true if other is the same Community + * as this object, false otherwise + * + * @param other + * object to compare to + * + * @return true if object passed in represents the same + * community as this object + */ + @Override + public boolean equals(Object other) + { + if (other == null) + { + return false; + } + Class objClass = HibernateProxyHelper.getClassWithoutInitializingProxy(other); + if (this.getClass() != objClass) + { + return false; + } + final Community otherCommunity = (Community) other; + if (!this.getID().equals(otherCommunity.getID() )) + { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return new HashCodeBuilder().append(getID()).toHashCode(); + } + + /** + * return type found in Constants + * @return Community type + */ + @Override + public int getType() + { + return Constants.COMMUNITY; + } + + @Override + public String getName() { + String value = getCommunityService().getMetadataFirstValue(this, MetadataSchema.DC_SCHEMA, "title", null, Item.ANY); + return value == null ? "" : value; + } + + @Override + public Integer getLegacyId() { + return legacyId; + } + + private CommunityService getCommunityService() { + if(communityService == null) + { + communityService = ContentServiceFactory.getInstance().getCommunityService(); + } + return communityService; + } +} \ No newline at end of file diff --git a/dspace/modules/additions/src/main/java/org/dspace/sort/OrderFormatAuthor.java b/dspace/modules/additions/src/main/java/org/dspace/sort/OrderFormatAuthor.java new file mode 100644 index 0000000000..0d41a3fb53 --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/sort/OrderFormatAuthor.java @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.sort; + +import org.dspace.text.filter.DecomposeDiactritics; +import org.dspace.text.filter.LowerCaseAndTrim; +import org.dspace.text.filter.StripDiacritics; +import org.dspace.text.filter.TextFilter; +import org.dspace.sort.AbstractTextFilterOFD; + +/** + * Standard author ordering delegate implementation + * + * @author Graham Triggs + */ +public class OrderFormatAuthor extends AbstractTextFilterOFD +{ + { + filters = new TextFilter[] { new DecomposeDiactritics(), + new StripDiacritics(), + new LowerCaseAndTrim() }; + } +} diff --git a/dspace/modules/additions/src/main/java/org/dspace/sort/OrderFormatText.java b/dspace/modules/additions/src/main/java/org/dspace/sort/OrderFormatText.java new file mode 100644 index 0000000000..85b86a27cb --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/sort/OrderFormatText.java @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.sort; + +import org.dspace.text.filter.DecomposeDiactritics; +import org.dspace.text.filter.StripDiacritics; +import org.dspace.text.filter.LowerCaseAndTrim; +import org.dspace.text.filter.TextFilter; +import org.dspace.sort.AbstractTextFilterOFD; + +/** + * Standard text ordering delegate implementation + * + * @author Graham Triggs + */ +public class OrderFormatText extends AbstractTextFilterOFD +{ + { + filters = new TextFilter[] { new DecomposeDiactritics(), + new StripDiacritics(), + new LowerCaseAndTrim() }; + } +} diff --git a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/images/favicon.ico b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/images/favicon.ico index 7274e66518..f353a5f98c 100644 Binary files a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/images/favicon.ico and b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/images/favicon.ico differ diff --git a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/_style.scss b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/_style.scss index 953079fe1e..0f7e03b071 100644 --- a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/_style.scss +++ b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/_style.scss @@ -69,6 +69,10 @@ width:200px; font-weight: 900; } +.a-ul { +text-decoration: underline; +} + // Altmetric styles .altmetric-embed { padding-top: 25px; diff --git a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/classic_mirage_color_scheme/_bootstrap_variables.scss b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/classic_mirage_color_scheme/_bootstrap_variables.scss index 3a9b9e5c47..53b4243c78 100644 --- a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/classic_mirage_color_scheme/_bootstrap_variables.scss +++ b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/styles/classic_mirage_color_scheme/_bootstrap_variables.scss @@ -10,15 +10,16 @@ // ---------------------- // VT Colors -$chicago-maroon: #660000; -$burnt-orange: #ff6600; +$chicago-maroon: #861F41; +$burnt-orange: #C64600; +$vt-blue-4t: #668AAA; // Web accessibility 2.0 compatible colors $gray-on-almost-white-aim: #494949; $gray-on-white-aim: #4c4c4c; -$content-link: #8c5206; -$content-link-hover: #12252c; +$content-link: $burnt-orange; +$content-link-hover: $vt-blue-4t; $content-text: #000000; $brand-primary: $chicago-maroon; diff --git a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/xsl/core/page-structure.xsl b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/xsl/core/page-structure.xsl index ce6e838926..677f40ea0e 100644 --- a/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/xsl/core/page-structure.xsl +++ b/dspace/modules/xmlui-mirage2/src/main/webapp/themes/vtmirage2/xsl/core/page-structure.xsl @@ -879,16 +879,15 @@ - - + + + diff --git a/dspace/modules/xmlui/src/main/java/org/dspace/app/xmlui/aspect/artifactbrowser/ConfigurableBrowse.java b/dspace/modules/xmlui/src/main/java/org/dspace/app/xmlui/aspect/artifactbrowser/ConfigurableBrowse.java index cbae891f05..b467f7f2bf 100644 --- a/dspace/modules/xmlui/src/main/java/org/dspace/app/xmlui/aspect/artifactbrowser/ConfigurableBrowse.java +++ b/dspace/modules/xmlui/src/main/java/org/dspace/app/xmlui/aspect/artifactbrowser/ConfigurableBrowse.java @@ -24,9 +24,11 @@ import org.dspace.app.xmlui.wing.Message; import org.dspace.app.xmlui.wing.WingException; import org.dspace.app.xmlui.wing.element.*; +import org.dspace.app.xmlui.wing.element.List; import org.dspace.authorize.AuthorizeException; import org.dspace.browse.*; import org.dspace.content.*; +import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.content.authority.factory.ContentAuthorityServiceFactory; import org.dspace.content.authority.service.ChoiceAuthorityService; @@ -41,10 +43,7 @@ import java.io.IOException; import java.io.Serializable; import java.sql.SQLException; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Implements all the browse functionality (browse by title, subject, authors, @@ -1101,7 +1100,10 @@ Map getControlParameters() throws UIException paramMap.put(BrowseParams.RESULTS_PER_PAGE, Integer .toString(this.scope.getResultsPerPage())); paramMap.put(BrowseParams.ETAL, Integer.toString(this.etAl)); - + if (this.scope.hasStartsWith()) { + paramMap.put(BrowseParams.STARTS_WITH, this.scope.getStartsWith()); + } + return paramMap; } diff --git a/dspace/modules/xmlui/src/main/resources/aspects/Discovery/i18n/messages.xml b/dspace/modules/xmlui/src/main/resources/aspects/Discovery/i18n/messages.xml index 01f322128c..4460dd852f 100644 --- a/dspace/modules/xmlui/src/main/resources/aspects/Discovery/i18n/messages.xml +++ b/dspace/modules/xmlui/src/main/resources/aspects/Discovery/i18n/messages.xml @@ -51,7 +51,6 @@ Committee Member Thesis Degree Level Department - Series Patent Type Inventor Assignee @@ -87,6 +86,7 @@ Series + Browsing by: Author Browsing by: Title Browsing by: Subject @@ -109,15 +109,21 @@ Browsing by: Department Browsing by: Issue date + Title Date issued Keyword Abstract - Series - Mime-Type - Sponsor + Call Number Identifier + Journal Title Language (ISO) + Publisher + Rights + Sponsor + Series + Version + Mime-Type Coverage Technical Report No. Committee Member @@ -138,6 +144,7 @@ Now showing items {0}-{1} + Filter by: Author Author Author @@ -160,8 +167,6 @@ Filter by: Assignee Filter by: Inventor Filter by: Patent Type - Date Issued - Starts with Or enter first few letters: diff --git a/dspace/modules/xmlui/src/main/webapp/i18n/messages.xml b/dspace/modules/xmlui/src/main/webapp/i18n/messages.xml index bd193b85b6..832bfb18bd 100644 --- a/dspace/modules/xmlui/src/main/webapp/i18n/messages.xml +++ b/dspace/modules/xmlui/src/main/webapp/i18n/messages.xml @@ -1586,8 +1586,9 @@ Metadata Files Thumbnails Licenses - Movie Posters + Movie Posters Creative Commons Licenses + SWORD File Please enter the name of the file on your computer corresponding to your item. If you click "Browse...", a new window will appear in which you can locate and select the file from your computer. Description