diff --git a/dspace-api/src/main/java/org/dspace/beacon/BeaconFileScript.java b/dspace-api/src/main/java/org/dspace/beacon/BeaconFileScript.java new file mode 100644 index 00000000000..5528d97cf15 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/beacon/BeaconFileScript.java @@ -0,0 +1,226 @@ +/** + * 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.beacon; + +import static org.dspace.core.Constants.READ; +import static org.dspace.eperson.Group.ANONYMOUS; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Date; +import java.sql.SQLException; +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; +import java.util.UUID; + +import org.apache.commons.cli.ParseException; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; + +/** + * Script to generate the beacon list + * The configuration for the metadata field and the resolven can be taken from the beacon.cfg file + * It is possible to use one field for some identifier (e.g. dc.identifier.other) and identify the corresponding values + * using their resolver, + * e.g. all values in dc.identifier.other which start with ... . + * Alternative resolvers can be + * specifed (optionally) which are later normalized to the main identifier + * Output + * - verbose: on handler log for debugging + * - file: print result as process result file with the given filename from the parameter + * For the specification see: ... + * + * @author Florian Gantner (florian.gantner@uni-bamberg.de) + * + */ +public class BeaconFileScript extends DSpaceRunnable> { + + private AuthorizeService authorizeService; + + private ItemService itemService; + + private ConfigurationService configurationService; + + private Context context; + + private boolean VERBOSE; + + private String PRINTFILE = null; + + private String metadatafield; + + @Override + public void setup() throws ParseException { + this.authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + this.itemService = ContentServiceFactory.getInstance().getItemService(); + this.configurationService = new DSpace().getConfigurationService(); + this.metadatafield = configurationService.getProperty("beacon.metadatafield"); + this.VERBOSE = commandLine.hasOption('v'); + if (commandLine.hasOption('f')) { + this.PRINTFILE = commandLine.getOptionValue('f'); + } + } + + @Override + public void internalRun() throws Exception { + + if ((!VERBOSE && PRINTFILE == null) || + (VERBOSE && PRINTFILE != null)) { + throw new Exception("Only one of the output options can be specified"); + } + + if (VERBOSE) { + context = new Context(Context.Mode.READ_ONLY); + } else if (PRINTFILE != null) { + context = new Context(Context.Mode.READ_WRITE); + } + + String mainresolver = configurationService.getProperty("beacon.mainresolver"); + String[] additionalresolvers = configurationService.getArrayProperty("beacon.additionalresolver"); + + assignCurrentUserInContext(); + assignSpecialGroupsInContext(); + + if (!this.authorizeService.isAdmin(context)) { + throw new IllegalArgumentException("The user cannot generate the beacon file"); + } + + StringBuilder sb = new StringBuilder(); + + sb.append("#FORMAT: Beacon").append(System.lineSeparator()); + for (String propertykey : configurationService.getPropertyKeys("beacon.header")) { + String key = propertykey.replace("beacon.header.", ""); + String value = configurationService.getProperty(propertykey); + if (value != null) { + sb.append("#").append(key.toUpperCase()).append(": ").append(value).append(System.lineSeparator()); + } + } + + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat sdf; + sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + sdf.setTimeZone(TimeZone.getTimeZone(ZoneId.of("Europe/Berlin"))); + String textdate = sdf.format(date); + + sb.append("#TIMESTAMP: ").append(textdate).append(System.lineSeparator()); + try { + Iterator items = itemService.findArchivedByMetadataField(context, metadatafield, Item.ANY); + List itemlist = new ArrayList<>(); + // filter inactive/hidden profiles + + while (items.hasNext()) { + Item item = items.next(); + if (!item.isDiscoverable() || !isVisible(item)) { + continue; + } + itemlist.add(item); + } + + //Normalize identifier. remove resolver or additionalresolver + for (Item item : itemlist) { + List mvals = itemService.getMetadataByMetadataString(item, metadatafield); + for (MetadataValue mval : mvals) { + //filter all values not starting with any of the resolver + String val = mval.getValue(); + if (configurationService.getBooleanProperty("beacon.metadatafield.filterresolver")) { + boolean skip = !val.startsWith(mainresolver); + //check main resolver + //check additional resolvers + if (additionalresolvers != null) { + for (String additionalresolver : additionalresolvers) { + if (val.startsWith(additionalresolver)) { + skip = false; + break; + } + } + } + if (skip) { + continue; + } + } + //normalize identifiers and replace the values + if (val != null && val.startsWith(mainresolver)) { + val = val.replace(mainresolver, ""); + } else if (val != null && additionalresolvers != null) { + for (String additionalresolver : additionalresolvers) { + if (val.startsWith(additionalresolver)) { + val = val.replace(additionalresolver, ""); + break; + } + } + } + sb.append(val).append(System.lineSeparator()); + break; + } + + } + String result = sb.toString(); + if (VERBOSE) { + handler.logInfo(result); + } else if (PRINTFILE != null) { + try { + handler.writeFilestream(context, PRINTFILE, + new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8)), "BEACON"); + } catch (Exception e) { + handler.logError(e.getMessage()); + } + } + context.complete(); + handler.logInfo("Beacon file completed successfully"); + } catch (Exception e) { + handler.handleException(e); + context.abort(); + } finally { + if (context.isValid()) { + context.close(); + } + } + } + + @Override + @SuppressWarnings("unchecked") + public BeaconFileScriptConfiguration getScriptConfiguration() { + return new DSpace().getServiceManager().getServiceByName("beacon-file", + BeaconFileScriptConfiguration.class); + } + + private void assignCurrentUserInContext() throws SQLException { + UUID uuid = getEpersonIdentifier(); + if (uuid != null) { + EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid); + context.setCurrentUser(ePerson); + } + } + + private void assignSpecialGroupsInContext() throws SQLException { + for (UUID uuid : handler.getSpecialGroups()) { + context.setSpecialGroup(uuid); + } + } + + public boolean isVisible(Item item) { + return item.getResourcePolicies().stream() + .filter(policy -> policy.getGroup() != null) + .anyMatch(policy -> READ == policy.getAction() && ANONYMOUS.equals(policy.getGroup().getName())); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/beacon/BeaconFileScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/beacon/BeaconFileScriptConfiguration.java new file mode 100644 index 00000000000..8d27b87553d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/beacon/BeaconFileScriptConfiguration.java @@ -0,0 +1,67 @@ +/** + * 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.beacon; + +import java.sql.SQLException; + +import org.apache.commons.cli.Options; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Script configuration for {@link BeaconFileScript}. + * + * @author Florian Gantner (florian.gantner@uni-bamberg.de) + */ +public class BeaconFileScriptConfiguration extends ScriptConfiguration { + + @Autowired + private AuthorizeService authorizeService; + + private Class dspaceRunnableClass; + + @Override + public boolean isAllowedToExecute(Context context) { + try { + return authorizeService.isAdmin(context); + } catch (SQLException e) { + throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); + } + } + + @Override + public Options getOptions() { + if (options == null) { + Options options = new Options(); + + options.addOption("v", "verbose", false, "print out result on handler log"); + options.getOption("v").setType(boolean.class); + options.getOption("v").setRequired(false); + + options.addOption("f", "file", true, "print to file and specify filename, e.g. beacon.txt"); + options.getOption("f").setType(String.class); + options.getOption("f").setRequired(false); + + super.options = options; + } + return options; + } + + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + +} diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d78479c2f23..32d024c099a 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1997,3 +1997,4 @@ include = ${module_dir}/pushocr.cfg include = ${module_dir}/pushocr.force.cfg include = ${module_dir}/cleanup-authority-metadata-relation.cfg include = ${module_dir}/ror.cfg +include = ${module_dir}/beacon.cfg diff --git a/dspace/config/modules/beacon.cfg b/dspace/config/modules/beacon.cfg new file mode 100644 index 00000000000..eb301d5db2f --- /dev/null +++ b/dspace/config/modules/beacon.cfg @@ -0,0 +1,28 @@ +# These Properties fill the header lines in the generated beacon file +# the metadatafield containing the gnd identifier +beacon.metadatafield = dc.identifier.gnd + +# if enabled filter the values with those matching the mainresolver or additionalresolver.additionalresolver +# this can be applied when the identifier field contains multiple identifiers, e.g. dc.identifier.other with prefixes +#beacon.metadatafield.filterresolver + +# the main resolver +# values from beacon.additionalresolver are normalized to this value +beacon.mainresolver = http://d-nb.info/gnd/ +# these are additional resolvers which are normalized. repeatable +#beacon.additionalresolver = https://d-nb.info/gnd/ +beacon.additionalresolver = https://d-nb.info/gnd/ + +## Header Lines +# FORMAT AND TIMESTAMP will always be added. +# the keys under beacon.header will be used for the HEADER Line starting with # +beacon.header.description = Professorinnen- und Professorenkatalog der Otto-Friedrich-Universität Bamberg +beacon.header.homepage = https://professorenkatalog.uni-bamberg.de +#beacon.header.contact = noreply@example.com +beacon.header.creator = Professorinnen- und Professorenkatalog der Otto-Friedrich-Universität Bamberg +beacon.header.message = Professorinnen- und Professorenkatalog der Otto-Friedrich-Universität Bamberg +beacon.header.prefix = ${beacon.mainresolver}{ID} +# target link. might differ when the gnd can be resolved under some own path, e.g. /gnd/{ID} +beacon.header.target = https://professorenkatalog.uni-bamberg.de/search?configuration\=default&q\=dc.identifier.gnd:{ID} +beacon.header.feed = https://professorenkatalog.uni-bamberg.de/beacon +beacon.header.update = MONTHLY diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index f4f7c97c001..08c9ce15b0a 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -165,4 +165,9 @@ + + + + +