diff --git a/src/main/java/edu/tamu/app/controller/DocumentController.java b/src/main/java/edu/tamu/app/controller/DocumentController.java index d7ffacf0..66599457 100644 --- a/src/main/java/edu/tamu/app/controller/DocumentController.java +++ b/src/main/java/edu/tamu/app/controller/DocumentController.java @@ -3,7 +3,6 @@ import static edu.tamu.weaver.response.ApiStatus.ERROR; import static edu.tamu.weaver.response.ApiStatus.SUCCESS; -import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -169,16 +168,29 @@ public ApiResponse save(@RequestBody Document document) { public ApiResponse push(@PathVariable String projectName, @PathVariable String documentName) { Document document = documentRepo.findByProjectNameAndName(projectName, documentName); + if (document.isPublishing()) { + return new ApiResponse(ERROR, "Cannot publish because document is already pending publication"); + } + + document.isPublishing(true); + documentRepo.update(document); + for (ProjectRepository repository : document.getProject().getRepositories()) { try { document = ((Destination) projectServiceRegistry.getService(repository.getName())).push(document); - } catch (IOException e) { + } catch (Exception e) { + document.isPublishing(false); + documentRepo.update(document); + logger.error("Exception thrown attempting to push to " + repository.getName() + "!", e); e.printStackTrace(); return new ApiResponse(ERROR, "There was an error publishing this item"); } } + document.isPublishing(false); + documentRepo.update(document); + return new ApiResponse(SUCCESS, "Your item has been successfully published", document); } diff --git a/src/main/java/edu/tamu/app/controller/ProjectController.java b/src/main/java/edu/tamu/app/controller/ProjectController.java index b7a481b0..f096558f 100644 --- a/src/main/java/edu/tamu/app/controller/ProjectController.java +++ b/src/main/java/edu/tamu/app/controller/ProjectController.java @@ -38,9 +38,10 @@ import edu.tamu.app.model.repo.ProjectRepo; import edu.tamu.app.model.repo.ProjectRepositoryRepo; import edu.tamu.app.model.repo.ProjectSuggestorRepo; +import edu.tamu.app.model.repo.ResourceRepo; import edu.tamu.app.service.ProjectFactory; +import edu.tamu.app.service.PropertyProtectionService; import edu.tamu.app.service.SyncService; -import edu.tamu.app.model.repo.ResourceRepo; import edu.tamu.app.service.registry.MagpieServiceRegistry; import edu.tamu.app.service.repository.Destination; import edu.tamu.app.utilities.FileSystemUtility; @@ -86,6 +87,9 @@ public class ProjectController { @Autowired private SyncService syncService; + @Autowired + private PropertyProtectionService propertyProtectionService; + /** * Endpoint to return list of projects. * @@ -116,8 +120,41 @@ public ApiResponse create(@WeaverValidatedModel Project project) throws IOExcept @RequestMapping("/update") @PreAuthorize("hasRole('MANAGER')") public ApiResponse update(@WeaverValidatedModel Project project) { + Project currentProject = projectRepo.findOne(project.getId()); boolean refreshProjectListener = (currentProject.isHeadless() != project.isHeadless()); + + //we need to populate the values of any protected ProjectService properties by getting the full entities from the repo + List projectRepositoryIds = new ArrayList(); + project.getRepositories().forEach(pr -> { + projectRepositoryIds.add(pr.getId()); + }); + project.setRepositories(projectRepositoryRepo.findAll(projectRepositoryIds)); + + project.getRepositories().forEach(r -> { + r.setPropertyProtectionService(propertyProtectionService); + }); + + List projectAuthorityIds = new ArrayList(); + project.getAuthorities().forEach(pa -> { + projectAuthorityIds.add(pa.getId()); + }); + project.setAuthorities(projectAuthorityRepo.findAll(projectAuthorityIds)); + + project.getAuthorities().forEach(a -> { + a.setPropertyProtectionService(propertyProtectionService); + }); + + List projectSuggestorIds = new ArrayList(); + project.getSuggestors().forEach(ps -> { + projectSuggestorIds.add(ps.getId()); + }); + project.setSuggestors(projectSuggestorRepo.findAll(projectSuggestorIds)); + + project.getSuggestors().forEach(s -> { + s.setPropertyProtectionService(propertyProtectionService); + }); + BeanUtils.copyProperties(project, currentProject, "documents","profiles"); currentProject = projectRepo.update(currentProject); projectFactory.registerServiceListeners(currentProject); diff --git a/src/main/java/edu/tamu/app/model/Document.java b/src/main/java/edu/tamu/app/model/Document.java index d4e06d47..8a780159 100644 --- a/src/main/java/edu/tamu/app/model/Document.java +++ b/src/main/java/edu/tamu/app/model/Document.java @@ -41,6 +41,9 @@ public class Document extends BaseEntity { @Column(nullable = true) private String path; + @Column(nullable = false) + private Boolean publishing; + @ManyToOne(optional = false) @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, scope = Project.class, resolver = ProjectByNameResolver.class, property = "name") @JsonIdentityReference(alwaysAsId = true) @@ -57,6 +60,7 @@ public class Document extends BaseEntity { public Document() { fields = new ArrayList(); publishedLocations = new ArrayList(); + publishing = false; } public Document(Project project, String name, String path, String status) { @@ -170,4 +174,12 @@ public MetadataFieldGroup getFieldByLabel(String labelName) { return targetField; } + public Boolean isPublishing() { + return publishing; + } + + public void isPublishing(Boolean publishing) { + this.publishing = publishing; + } + } diff --git a/src/main/java/edu/tamu/app/model/ProjectService.java b/src/main/java/edu/tamu/app/model/ProjectService.java index 72719242..aced72b6 100644 --- a/src/main/java/edu/tamu/app/model/ProjectService.java +++ b/src/main/java/edu/tamu/app/model/ProjectService.java @@ -1,5 +1,7 @@ package edu.tamu.app.model; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; @@ -9,10 +11,12 @@ import javax.persistence.FetchType; import javax.persistence.MappedSuperclass; import javax.persistence.OneToMany; +import javax.persistence.Transient; import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; +import edu.tamu.app.service.PropertyProtectionService; import edu.tamu.weaver.validation.model.ValidatingBaseEntity; @MappedSuperclass @@ -28,6 +32,9 @@ public abstract class ProjectService extends ValidatingBaseEntity { @Fetch(FetchMode.SELECT) private List settings; + @Transient + private PropertyProtectionService propertyProtectionService = null; + public ProjectService() { settings = new ArrayList(); } @@ -57,14 +64,31 @@ public void setSettings(List settings) { } public List getSettingValues(String key) { - List targetSetting = null; + List targetSettingValues = null; + boolean isProtect = false; for (ProjectSetting setting : settings) { if (setting.getKey().equals(key)) { - targetSetting = setting.getValues(); + isProtect = setting.isProtect(); + targetSettingValues = setting.getValues(); break; } } - return targetSetting; + if (propertyProtectionService != null && isProtect) { + try { + targetSettingValues = propertyProtectionService.decryptPropertyValues(targetSettingValues); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + } + return targetSettingValues; + } + + public PropertyProtectionService getPropertyProtectionService() { + return propertyProtectionService; + } + + public void setPropertyProtectionService(PropertyProtectionService propertyProtectionService) { + this.propertyProtectionService = propertyProtectionService; } } diff --git a/src/main/java/edu/tamu/app/model/ProjectSetting.java b/src/main/java/edu/tamu/app/model/ProjectSetting.java index a3be9649..b9940c48 100644 --- a/src/main/java/edu/tamu/app/model/ProjectSetting.java +++ b/src/main/java/edu/tamu/app/model/ProjectSetting.java @@ -8,6 +8,8 @@ import javax.persistence.Entity; import javax.persistence.FetchType; +import com.fasterxml.jackson.annotation.JsonGetter; + import edu.tamu.weaver.data.model.BaseEntity; @Entity @@ -19,6 +21,9 @@ public class ProjectSetting extends BaseEntity { @ElementCollection(fetch = FetchType.EAGER) private List values; + @Column + private Boolean protect = false; + public ProjectSetting() { setValues(new ArrayList()); } @@ -28,6 +33,12 @@ public ProjectSetting(String key, List values) { setValues(values); } + public ProjectSetting(String key, List values, Boolean protect) { + setKey(key); + setValues(values); + setProtect(protect); + } + public String getKey() { return key; } @@ -40,6 +51,19 @@ public List getValues() { return values; } + @JsonGetter("values") + protected List getValuesForSerializer() { + if (this.isProtect()) { + ArrayList protectedValues = new ArrayList(); + this.getValues().forEach(k -> { + protectedValues.add(""); + }); + return protectedValues; + } else { + return getValues(); + } + } + public void setValues(List values) { this.values = values; } @@ -54,4 +78,12 @@ public void removeValue(String value) { this.values.remove(value); } + public Boolean isProtect() { + return protect; + } + + public void setProtect(Boolean protect) { + this.protect = protect; + } + } diff --git a/src/main/java/edu/tamu/app/model/PublishingEvent.java b/src/main/java/edu/tamu/app/model/PublishingEvent.java new file mode 100644 index 00000000..668d95e9 --- /dev/null +++ b/src/main/java/edu/tamu/app/model/PublishingEvent.java @@ -0,0 +1,31 @@ +package edu.tamu.app.model; + +import java.util.Date; + +public class PublishingEvent { + + private final PublishingType type; + + private final String message; + + private final Date timestamp; + + public PublishingEvent(PublishingType type, String message) { + this.type = type; + this.message = message; + timestamp = new Date(System.currentTimeMillis()); + } + + public PublishingType getType() { + return type; + } + + public String getMessage() { + return message; + } + + public Date getTimestamp() { + return timestamp; + } + +} diff --git a/src/main/java/edu/tamu/app/model/PublishingType.java b/src/main/java/edu/tamu/app/model/PublishingType.java new file mode 100644 index 00000000..54e7f56b --- /dev/null +++ b/src/main/java/edu/tamu/app/model/PublishingType.java @@ -0,0 +1,5 @@ +package edu.tamu.app.model; + +public enum PublishingType { + ALERT, ATTACHMENT, CONNECTION, ITEM, MESSAGE, WARNING +} diff --git a/src/main/java/edu/tamu/app/model/repo/impl/ProjectAuthorityRepoImpl.java b/src/main/java/edu/tamu/app/model/repo/impl/ProjectAuthorityRepoImpl.java index d44764c6..59238119 100644 --- a/src/main/java/edu/tamu/app/model/repo/impl/ProjectAuthorityRepoImpl.java +++ b/src/main/java/edu/tamu/app/model/repo/impl/ProjectAuthorityRepoImpl.java @@ -1,5 +1,7 @@ package edu.tamu.app.model.repo.impl; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; @@ -12,6 +14,7 @@ import edu.tamu.app.model.repo.ProjectAuthorityRepo; import edu.tamu.app.model.repo.ProjectRepo; import edu.tamu.app.model.repo.custom.ProjectAuthorityRepoCustom; +import edu.tamu.app.service.PropertyProtectionService; import edu.tamu.weaver.data.model.repo.impl.AbstractWeaverRepoImpl; public class ProjectAuthorityRepoImpl extends AbstractWeaverRepoImpl implements ProjectAuthorityRepoCustom { @@ -22,6 +25,14 @@ public class ProjectAuthorityRepoImpl extends AbstractWeaverRepoImpl v.equals(""))) { + try { + setting.setValues(propertyProtectionService.decryptPropertyValues(currentProjectAuthority.getSettingValues(setting.getKey()))); + projectAuthority.getSettings().set(i, setting); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + } + } + return super.update(processProjectAuthority(projectAuthority)); } @Override @@ -52,4 +80,18 @@ public void delete(ProjectAuthority projectAuthority) { protected String getChannel() { return "/channel/project-authority"; } + + private ProjectAuthority processProjectAuthority(ProjectAuthority projectAuthority) { + projectAuthority.setPropertyProtectionService(propertyProtectionService); + projectAuthority.getSettings().forEach(s -> { + if (s.isProtect()) { + try { + s.setValues(propertyProtectionService.encryptPropertyValues(s.getValues())); + } catch (GeneralSecurityException | IOException e1) { + e1.printStackTrace(); + } + } + }); + return projectAuthority; + } } diff --git a/src/main/java/edu/tamu/app/model/repo/impl/ProjectRepositoryRepoImpl.java b/src/main/java/edu/tamu/app/model/repo/impl/ProjectRepositoryRepoImpl.java index 4d9a5bc9..f7b19a49 100644 --- a/src/main/java/edu/tamu/app/model/repo/impl/ProjectRepositoryRepoImpl.java +++ b/src/main/java/edu/tamu/app/model/repo/impl/ProjectRepositoryRepoImpl.java @@ -1,5 +1,7 @@ package edu.tamu.app.model.repo.impl; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; @@ -12,6 +14,7 @@ import edu.tamu.app.model.repo.ProjectRepo; import edu.tamu.app.model.repo.ProjectRepositoryRepo; import edu.tamu.app.model.repo.custom.ProjectRepositoryRepoCustom; +import edu.tamu.app.service.PropertyProtectionService; import edu.tamu.weaver.data.model.repo.impl.AbstractWeaverRepoImpl; public class ProjectRepositoryRepoImpl extends AbstractWeaverRepoImpl implements ProjectRepositoryRepoCustom { @@ -22,6 +25,14 @@ public class ProjectRepositoryRepoImpl extends AbstractWeaverRepoImpl v.equals(""))) { + try { + setting.setValues(propertyProtectionService.decryptPropertyValues(currentProjectRepository.getSettingValues(setting.getKey()))); + projectRepository.getSettings().set(i, setting); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + } + } + return super.update(processProjectRepository(projectRepository)); } @Override @@ -52,4 +80,18 @@ public void delete(ProjectRepository projectRepository) { protected String getChannel() { return "/channel/project-repository"; } + + private ProjectRepository processProjectRepository(ProjectRepository projectRepository) { + projectRepository.setPropertyProtectionService(propertyProtectionService); + projectRepository.getSettings().forEach(s -> { + if (s.isProtect()) { + try { + s.setValues(propertyProtectionService.encryptPropertyValues(s.getValues())); + } catch (GeneralSecurityException | IOException e1) { + e1.printStackTrace(); + } + } + }); + return projectRepository; + } } diff --git a/src/main/java/edu/tamu/app/model/repo/impl/ProjectSuggestorRepoImpl.java b/src/main/java/edu/tamu/app/model/repo/impl/ProjectSuggestorRepoImpl.java index 3143a5b0..5ae094e3 100644 --- a/src/main/java/edu/tamu/app/model/repo/impl/ProjectSuggestorRepoImpl.java +++ b/src/main/java/edu/tamu/app/model/repo/impl/ProjectSuggestorRepoImpl.java @@ -1,5 +1,7 @@ package edu.tamu.app.model.repo.impl; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; @@ -12,6 +14,7 @@ import edu.tamu.app.model.repo.ProjectRepo; import edu.tamu.app.model.repo.ProjectSuggestorRepo; import edu.tamu.app.model.repo.custom.ProjectSuggestorRepoCustom; +import edu.tamu.app.service.PropertyProtectionService; import edu.tamu.weaver.data.model.repo.impl.AbstractWeaverRepoImpl; public class ProjectSuggestorRepoImpl extends AbstractWeaverRepoImpl implements ProjectSuggestorRepoCustom { @@ -22,6 +25,14 @@ public class ProjectSuggestorRepoImpl extends AbstractWeaverRepoImpl v.equals(""))) { + try { + setting.setValues(propertyProtectionService.decryptPropertyValues(currentProjectSuggestor.getSettingValues(setting.getKey()))); + projectSuggestor.getSettings().set(i, setting); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + } + } + return super.update(processProjectSuggestor(projectSuggestor)); } @Override @@ -52,4 +80,18 @@ public void delete(ProjectSuggestor projectSuggestor) { protected String getChannel() { return "/channel/project-suggestor"; } + + private ProjectSuggestor processProjectSuggestor(ProjectSuggestor projectSuggestor) { + projectSuggestor.setPropertyProtectionService(propertyProtectionService); + projectSuggestor.getSettings().forEach(s -> { + if (s.isProtect()) { + try { + s.setValues(propertyProtectionService.encryptPropertyValues(s.getValues())); + } catch (GeneralSecurityException | IOException e1) { + e1.printStackTrace(); + } + } + }); + return projectSuggestor; + } } diff --git a/src/main/java/edu/tamu/app/service/PropertyProtectionService.java b/src/main/java/edu/tamu/app/service/PropertyProtectionService.java new file mode 100644 index 00000000..a3a2526f --- /dev/null +++ b/src/main/java/edu/tamu/app/service/PropertyProtectionService.java @@ -0,0 +1,103 @@ +package edu.tamu.app.service; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class PropertyProtectionService { + @Value("${app.security.secret:verysecretsecret}") + private String secret; + + @Value("${app.security.propertySalt:13ksloe*}") + private String propertySalt; + + private final static String ENCRYPTION_ALGORITHM = "AES"; + private final static String SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA512"; + private final static String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding"; + + private final static int iterationCount = 40000; + private final static int keyLength = 128; + + public String encryptPropertyValue(String propertyValue) throws UnsupportedEncodingException, GeneralSecurityException { + return encrypt(propertyValue, createSecretKey(secret.toCharArray(), propertySalt.getBytes(), iterationCount, keyLength)); + } + + public List encryptPropertyValues(List propertyValues) throws NoSuchAlgorithmException, InvalidKeySpecException, GeneralSecurityException, IOException { + List protectedValues = new ArrayList(); + propertyValues.forEach(v -> { + try { + protectedValues.add(encryptPropertyValue(v)); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + }); + return protectedValues; + } + + + public String decryptPropertyValue(String propertyValue) throws NoSuchAlgorithmException, InvalidKeySpecException, GeneralSecurityException, IOException { + return decrypt(propertyValue, createSecretKey(secret.toCharArray(), propertySalt.getBytes(), iterationCount, keyLength)); + } + + public List decryptPropertyValues(List propertyValues) throws NoSuchAlgorithmException, InvalidKeySpecException, GeneralSecurityException, IOException { + List protectedValues = new ArrayList(); + propertyValues.forEach(v -> { + try { + protectedValues.add(decryptPropertyValue(v)); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + }); + return protectedValues; + } + + private static SecretKeySpec createSecretKey(char[] password, byte[] salt, int iterationCount, int keyLength) throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM); + PBEKeySpec keySpec = new PBEKeySpec(password, salt, iterationCount, keyLength); + SecretKey keyTmp = keyFactory.generateSecret(keySpec); + return new SecretKeySpec(keyTmp.getEncoded(), ENCRYPTION_ALGORITHM); + } + + private static String encrypt(String property, SecretKeySpec key) throws GeneralSecurityException, UnsupportedEncodingException { + Cipher pbeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + pbeCipher.init(Cipher.ENCRYPT_MODE, key); + AlgorithmParameters parameters = pbeCipher.getParameters(); + IvParameterSpec ivParameterSpec = parameters.getParameterSpec(IvParameterSpec.class); + byte[] cryptoText = pbeCipher.doFinal(property.getBytes("UTF-8")); + byte[] iv = ivParameterSpec.getIV(); + return base64Encode(iv) + ":" + base64Encode(cryptoText); + } + + private static String base64Encode(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private static String decrypt(String string, SecretKeySpec key) throws GeneralSecurityException, IOException { + String iv = string.split(":")[0]; + String property = string.split(":")[1]; + Cipher pbeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + pbeCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(base64Decode(iv))); + return new String(pbeCipher.doFinal(base64Decode(property)), "UTF-8"); + } + + private static byte[] base64Decode(String property) throws IOException { + return Base64.getDecoder().decode(property); + } +} \ No newline at end of file diff --git a/src/main/java/edu/tamu/app/service/repository/DSpaceRepository.java b/src/main/java/edu/tamu/app/service/repository/DSpaceRepository.java index 0cc8d093..f56c4627 100644 --- a/src/main/java/edu/tamu/app/service/repository/DSpaceRepository.java +++ b/src/main/java/edu/tamu/app/service/repository/DSpaceRepository.java @@ -15,7 +15,9 @@ import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import javax.xml.parsers.DocumentBuilder; @@ -52,11 +54,12 @@ import edu.tamu.app.model.MetadataFieldValue; import edu.tamu.app.model.ProjectRepository; import edu.tamu.app.model.PublishedLocation; +import edu.tamu.app.model.PublishingType; import edu.tamu.app.model.Resource; import edu.tamu.app.model.repo.DocumentRepo; import edu.tamu.app.model.repo.ResourceRepo; -public class DSpaceRepository implements Repository { +public class DSpaceRepository extends PublishingRepository { private static final Logger logger = Logger.getLogger(DSpaceRepository.class); @@ -71,24 +74,25 @@ public class DSpaceRepository implements Repository { private ProjectRepository projectRepository; - private Optional authCookie; + private Map> authCookies; public DSpaceRepository(ProjectRepository projectRepository) { this.projectRepository = projectRepository; - authCookie = Optional.empty(); + this.authCookies = new HashMap>(); } @Override public Document push(Document document) throws IOException { - // login to get JSESSIONID - login(); + login(document); // POST to create the item JsonNode createItemResponseNode = null; try { createItemResponseNode = createItem(document); } catch (ParserConfigurationException | TransformerException | IOException e) { + logout(document); + RuntimeException serviceEx = new RuntimeException(e.getMessage()); serviceEx.setStackTrace(e.getStackTrace()); throw serviceEx; @@ -97,8 +101,10 @@ public Document push(Document document) throws IOException { String handleString = createItemResponseNode.get("handle").asText(); String newItemIdString = createItemResponseNode.get("uuid").asText(); + broadcastDocument(document.getId(), PublishingType.ITEM, "Created Item using Internal ID " + newItemIdString + "."); + // POST each of the bitstreams in this document to the newly created item - addBitstreams(newItemIdString, document); + addBitstreams(document, newItemIdString); // add new handle to document, change it's status to published, save it String publishedUrl; @@ -110,16 +116,17 @@ public Document push(Document document) throws IOException { } document.addPublishedLocation(new PublishedLocation(projectRepository, publishedUrl)); + broadcastDocument(document.getId(), PublishingType.MESSAGE, "Published at URL " + publishedUrl + "."); document.setStatus("Published"); // logout to kill session - logout(); + logout(document); return documentRepo.update(document); } - private void login() throws IOException { + private void login(Document document) throws IOException { try { HttpClient httpClient = null; CookieStore httpCookieStore = new BasicCookieStore(); @@ -138,15 +145,16 @@ private void login() throws IOException { for (Cookie cookie : httpCookieStore.getCookies()) { if (cookie.getName().equals("JSESSIONID")) { - authCookie = Optional.of(cookie); + authCookies.put(document.getId(), Optional.of(cookie)); } } - if (!authCookie.isPresent()) { + if (!authCookies.containsKey(document.getId()) || !authCookies.get(document.getId()).isPresent()) { throw new RuntimeException("Unable to get cookie JSESSIONID from response!"); } - logger.info("Login successful. Authorization cookie: " + getCookieAsString(authCookie.get())); + logger.info("Login successful. Authorization cookie: " + getCookieAsString(authCookies.get(document.getId()).get())); + broadcastDocument(document.getId(), PublishingType.CONNECTION, "DSpace session established for repository " + projectRepository.getName() + "."); } catch (IOException e) { IOException ioe = new IOException("Failed to authenticate to DSpace. {" + e.getMessage() + "}"); @@ -155,10 +163,11 @@ private void login() throws IOException { } } - private void logout() throws IOException { - doRESTRequest(new URL(getRepoUrl() + "/rest/logout"), "POST", "".getBytes(), "application/xml", "logout"); - authCookie = Optional.empty(); + private void logout(Document document) throws IOException { + doRESTRequest(document.getId(), new URL(getRepoUrl() + "/rest/logout"), "POST", "".getBytes(), "application/xml", "logout"); + authCookies.remove(document.getId()); logger.info("Logout successful."); + broadcastDocument(document.getId(), PublishingType.CONNECTION, "DSpace session closed for repository " + projectRepository.getName() + "."); } private JsonNode createItem(Document document) throws ParserConfigurationException, TransformerException, IOException { @@ -166,6 +175,8 @@ private JsonNode createItem(Document document) throws ParserConfigurationExcepti try { createItemUrl = new URL(getRepoUrl() + "/rest/collections/" + getCollectionId() + "/items"); } catch (MalformedURLException e) { + logout(document); + MalformedURLException murle = new MalformedURLException("Failed to create items; the REST URL to post the item was malformed. {" + e.getMessage() + "}"); murle.setStackTrace(e.getStackTrace()); throw murle; @@ -176,14 +187,20 @@ private JsonNode createItem(Document document) throws ParserConfigurationExcepti try { xmlDataToPost = generateItemPostXMLFromDocument(document); } catch (ParserConfigurationException e) { + logout(document); + ParserConfigurationException pce = new ParserConfigurationException("Failed to create items; Could not transform document metadata into XML for the post. {" + e.getMessage() + "}"); pce.setStackTrace(e.getStackTrace()); throw pce; } catch (TransformerFactoryConfigurationError e) { + logout(document); + TransformerFactoryConfigurationError tfce = new TransformerFactoryConfigurationError("Failed to create items; Could not transform document metadata into XML for the post. {" + e.getMessage() + "}"); tfce.setStackTrace(e.getStackTrace()); throw tfce; } catch (TransformerException e) { + logout(document); + TransformerException te = new TransformerException("Failed to create items; Could not transform document metadata into XML for the post. {" + e.getMessage() + "}"); te.setStackTrace(e.getStackTrace()); throw te; @@ -191,10 +208,10 @@ private JsonNode createItem(Document document) throws ParserConfigurationExcepti String taskDescription = "post item"; - return doRESTRequest(createItemUrl, "POST", xmlDataToPost.getBytes(), "application/xml", taskDescription); + return doRESTRequest(document.getId(), createItemUrl, "POST", xmlDataToPost.getBytes(), "application/xml", taskDescription); } - private JsonNode doRESTRequest(URL restUrl, String method, byte[] postData, String contentTypeString, String taskDescription) throws IOException { + private JsonNode doRESTRequest(Long documentId, URL restUrl, String method, byte[] postData, String contentTypeString, String taskDescription) throws IOException { logger.info("Making this REST request of DSpace: "+ taskDescription); // set up the connection for the REST call HttpURLConnection connection; @@ -220,8 +237,8 @@ private JsonNode doRESTRequest(URL restUrl, String method, byte[] postData, Stri connection.setRequestProperty("Content-Length", String.valueOf(postData.length)); - connection.setRequestProperty("Cookie", getCookieAsString(authCookie.get())); - + connection.setRequestProperty("Cookie", getCookieAsString(authCookies.get(documentId).get())); + logger.info("Attempting to connect to DSpace with Cookie = " + connection.getRequestProperty("Cookie")); connection.setDoOutput(true); @@ -299,13 +316,13 @@ private JsonNode doRESTRequest(URL restUrl, String method, byte[] postData, Stri return responseNode; } - private void addBitstreams(String itemId, Document document) throws IOException { - addBitstreams(new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "application/pdf"))); - addBitstreams(new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "text/plain"), "TEXT")); - addBitstreams(new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "image/jpeg", "image/jpg", "image/jp2", "image/jpx", "image/bmp", "image/gif", "image/png", "image/svg", "image/tif", "image/tiff"))); + private void addBitstreams(Document document, String itemId) throws IOException { + addBitstreams(document, new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "application/pdf"))); + addBitstreams(document, new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "text/plain"), "TEXT")); + addBitstreams(document, new Bitstreams(itemId, resourceRepo.findAllByDocumentProjectNameAndDocumentNameAndMimeType(document.getProject().getName(), document.getName(), "image/jpeg", "image/jpg", "image/jp2", "image/jpx", "image/bmp", "image/gif", "image/png", "image/svg", "image/tif", "image/tiff"))); } - private void addBitstreams(Bitstreams bitstreams) throws IOException { + private void addBitstreams(Document document, Bitstreams bitstreams) throws IOException { for (Resource resource : bitstreams.getResources()) { // ************************************* @@ -318,7 +335,7 @@ private void addBitstreams(Bitstreams bitstreams) throws IOException { } catch (MalformedURLException e) { MalformedURLException murle = new MalformedURLException("Failed to add pdf bitstream; the REST URL to post the bitstreams was malformed. {" + e.getMessage() + "}"); murle.setStackTrace(e.getStackTrace()); - cleanUpFailedPublish(bitstreams.getItemId()); + cleanUpFailedPublish(document, bitstreams.getItemId()); throw murle; } @@ -329,9 +346,9 @@ private void addBitstreams(Bitstreams bitstreams) throws IOException { ObjectNode bitstreamMetadataJson = null; try { - bitstreamMetadataJson = (ObjectNode) doRESTRequest(addBitstreamUrl, "POST", bytes, resource.getMimeType(), "post bitstream"); + bitstreamMetadataJson = (ObjectNode) doRESTRequest(document.getId(), addBitstreamUrl, "POST", bytes, resource.getMimeType(), "post bitstream"); } catch (Exception e) { - cleanUpFailedPublish(bitstreams.getItemId()); + cleanUpFailedPublish(document, bitstreams.getItemId()); throw e; } @@ -345,6 +362,8 @@ private void addBitstreams(Bitstreams bitstreams) throws IOException { String uuid = bitstreamMetadataJson.get("uuid").asText(); + broadcastDocument(document.getId(), PublishingType.ATTACHMENT, "Associated item " + resource.getName() + " using Internal ID " + uuid + "."); + ArrayNode policiesNode = bitstreamMetadataJson.putArray("policies"); ObjectNode policyNode = objectMapper.createObjectNode(); policyNode.put("action", "READ"); @@ -362,17 +381,18 @@ private void addBitstreams(Bitstreams bitstreams) throws IOException { } catch (MalformedURLException e) { MalformedURLException murle = new MalformedURLException("Failed to update bitstream metadata; the REST URL to PUT the policy was malformed. {" + e.getMessage() + "}"); murle.setStackTrace(e.getStackTrace()); - cleanUpFailedPublish(bitstreams.getItemId()); + cleanUpFailedPublish(document, bitstreams.getItemId()); throw murle; } try { - doRESTRequest(addPolicyUrl, "PUT", bitstreamMetadataJson.toString().getBytes(), "application/json", "update bitstream metadata"); + doRESTRequest(document.getId(), addPolicyUrl, "PUT", bitstreamMetadataJson.toString().getBytes(), "application/json", "update bitstream metadata"); } catch (Exception e) { - cleanUpFailedPublish(bitstreams.getItemId()); + cleanUpFailedPublish(document, bitstreams.getItemId()); throw e; } + broadcastDocument(document.getId(), PublishingType.MESSAGE, "Populated metadata for item " + resource.getName() + " using Internal ID " + uuid + "."); } } @@ -412,25 +432,36 @@ private String generateItemPostXMLFromDocument(Document document) throws ParserC return stw.toString(); } - private void cleanUpFailedPublish(String uuid) throws IOException { + private void cleanUpFailedPublish(Document document, String uuid) throws IOException { // delete the item in case there was an error along the way with all the requests. // REST endpoint is DELETE /items/{item uuid} - Delete item. logger.error("Error pushing to DSpace. Rolling back."); + broadcastDocument(document.getId(), PublishingType.ALERT, "Error pushing item " + uuid + " to DSpace. Rolling back."); URL deleteItemUrl; try { deleteItemUrl = new URL(getRepoUrl() + "/rest/items/" + uuid); } catch (MalformedURLException e) { + logout(document); + MalformedURLException murle = new MalformedURLException("Failed to delete item " + uuid + "; the REST URL for the DELETE request was malformed. {" + e.getMessage() + "}"); murle.setStackTrace(e.getStackTrace()); throw murle; } - doRESTRequest(deleteItemUrl, "DELETE", "".getBytes(), "application/json", "delete item"); + try { + doRESTRequest(document.getId(), deleteItemUrl, "DELETE", "".getBytes(), "application/json", "delete item"); + } + catch (IOException e) { + logout(document); + throw e; + } + + broadcastDocument(document.getId(), PublishingType.WARNING, "Cleaned up item " + uuid + "."); // logout to kill session - logout(); + logout(document); } class Bitstreams { @@ -492,11 +523,8 @@ public String getPassword() { } private String getSettingValue(String key) { - return hasSettingValues(key) ? projectRepository.getSettingValues(key).get(0) : ""; - } - - private boolean hasSettingValues(String key) { - return projectRepository.getSettingValues(key) != null && projectRepository.getSettingValues(key).size() > 0; + String value = projectRepository.getSettingValues(key).get(0); + return (value != null) ? value:""; } private String getCookieAsString(Cookie cookie) { diff --git a/src/main/java/edu/tamu/app/service/repository/PublishingRepository.java b/src/main/java/edu/tamu/app/service/repository/PublishingRepository.java new file mode 100644 index 00000000..e9879ebb --- /dev/null +++ b/src/main/java/edu/tamu/app/service/repository/PublishingRepository.java @@ -0,0 +1,26 @@ +package edu.tamu.app.service.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +import edu.tamu.app.model.PublishingEvent; +import edu.tamu.app.model.PublishingType; +import edu.tamu.weaver.response.ApiAction; +import edu.tamu.weaver.response.ApiResponse; +import edu.tamu.weaver.response.ApiStatus; + +public abstract class PublishingRepository implements Repository { + + @Autowired + private SimpMessagingTemplate simpMessagingTemplate; + + protected String getChannel() { + return "/channel/publishing"; + } + + protected void broadcastDocument(Long documentId, PublishingType type, String message) { + ApiResponse response = new ApiResponse(ApiStatus.SUCCESS, ApiAction.BROADCAST, new PublishingEvent(type, message)); + simpMessagingTemplate.convertAndSend(getChannel() + "/document/" + documentId, response); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 494e83c1..539df1d4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -103,6 +103,11 @@ auth.security.jwt.duration: 1 app.security.secret: verysecretsecret ################################################################ +################################################################ +# package edu.tamu.app.service.PropertyProtectionService +app.security.propertySalt: 1rk3l7d* +################################################################ + ################################################################ # edu.tamu.weaver.filter.CorsFilter app.security.allow-access: http://localhost,http://localhost:8080,http://labs.library.tamu.edu,http://machuff.tamu.edu,http://caerus.library.tamu.edu:8080,http://savell.evans.tamu.edu,http://jcreel.tamu.edu diff --git a/src/main/resources/config/projects.json b/src/main/resources/config/projects.json index 8446719b..a9d68995 100644 --- a/src/main/resources/config/projects.json +++ b/src/main/resources/config/projects.json @@ -236,7 +236,8 @@ "key": "password", "values": [ "" - ] + ], + "protect": true } ] }], @@ -279,7 +280,8 @@ "key": "password", "values": [ "" - ] + ], + "protect": true } ] }], @@ -327,7 +329,8 @@ "key": "password", "values": [ "" - ] + ], + "protect": true } ] }], @@ -459,7 +462,8 @@ "key": "password", "values": [ "" - ] + ], + "protect": true } ] }], @@ -543,7 +547,8 @@ "key": "password", "values": [ "" - ] + ], + "protect": true } ] }], @@ -719,7 +724,8 @@ "key": "password", "values": [ "" - ] + ], + "protect": true } ] }], diff --git a/src/main/resources/static/metadatatool/gendocs.sh b/src/main/resources/static/metadatatool/gendocs.sh index a38505d4..b34b903b 100755 --- a/src/main/resources/static/metadatatool/gendocs.sh +++ b/src/main/resources/static/metadatatool/gendocs.sh @@ -1,7 +1,7 @@ #!/bin/bash # Generates test documents if [ $# -eq 0 ]; then - echo "Usage: gendocs [project name] [number of documents] [number of files/doc] [media type: txt, pdf, jpg, tif]" + echo "Usage: gendocs [project name] [number of documents] [number of files/doc] [media type: txt, pdf, jpg, png, tif]" else if [ "$#" -eq 3 ]; then @@ -34,11 +34,15 @@ else if [[ " ${MEDIA_TYPES[@]} " =~ " pdf " ]] || [[ " ${MEDIA_TYPES[@]} " =~ " all " ]]; then cp ./sample-media/sample.pdf "projects/"$1"/"$1"_"$DOCCOUNT"/"$1"_"$DOCCOUNT"_"$FILECOUNT".pdf" fi - + if [[ " ${MEDIA_TYPES[@]} " =~ " jpg " ]] || [[ " ${MEDIA_TYPES[@]} " =~ " all " ]]; then cp ./sample-media/sample.jpg "projects/"$1"/"$1"_"$DOCCOUNT"/"$1"_"$DOCCOUNT"_"$FILECOUNT".jpg" fi - + + if [[ " ${MEDIA_TYPES[@]} " =~ " png " ]] || [[ " ${MEDIA_TYPES[@]} " =~ " all " ]]; then + cp ./sample-media/sample.png "projects/"$1"/"$1"_"$DOCCOUNT"/"$1"_"$DOCCOUNT"_"$FILECOUNT".png" + fi + if [[ " ${MEDIA_TYPES[@]} " =~ " tif " ]] || [[ " ${MEDIA_TYPES[@]} " =~ " all " ]]; then cp ./sample-media/sample.tif "projects/"$1"/"$1"_"$DOCCOUNT"/"$1"_"$DOCCOUNT"_"$FILECOUNT".tif"; echo "projects/"$1"/"$1"_"$DOCCOUNT"/"$1"_"$DOCCOUNT"_"$FILECOUNT".tif" diff --git a/src/main/resources/static/metadatatool/sample-media/sample.png b/src/main/resources/static/metadatatool/sample-media/sample.png new file mode 100644 index 00000000..b4b78734 Binary files /dev/null and b/src/main/resources/static/metadatatool/sample-media/sample.png differ diff --git a/src/test/java/edu/tamu/app/controller/AbstractControllerTest.java b/src/test/java/edu/tamu/app/controller/AbstractControllerTest.java index c3ed94d7..9cdb01f4 100644 --- a/src/test/java/edu/tamu/app/controller/AbstractControllerTest.java +++ b/src/test/java/edu/tamu/app/controller/AbstractControllerTest.java @@ -58,7 +58,7 @@ public abstract class AbstractControllerTest extends MockData { @Mock protected DocumentRepo documentRepo; - + @Mock protected ResourceRepo resourceRepo; @@ -189,6 +189,13 @@ public Document answer(InvocationOnMock invocation) throws Throwable { } }); + when(documentRepo.findOne(any(Long.class))).then(new Answer() { + @Override + public Document answer(InvocationOnMock invocation) throws Throwable { + return findDocumentById((Long) invocation.getArguments()[0]); + } + }); + // TODO when(documentRepo.pageableDynamicDocumentQuery((Map) any(Map.class), (org.springframework.data.domain.Pageable) any(Pageable.class))).then(new Answer>() { @Override @@ -202,7 +209,7 @@ public Page answer(InvocationOnMock invocation) throws Throwable { // project when(projectRepo.findAll()).thenReturn(mockProjectList); - when(projectRepo.create(any(String.class), any(IngestType.class), any(Boolean.class), (List) any(List.class), any(List.class), any(List.class))).then(new Answer() { + when(projectRepo.create(any(String.class), any(IngestType.class), any(Boolean.class), any(List.class), any(List.class), any(List.class))).then(new Answer() { @Override public Project answer(InvocationOnMock invocation) throws Throwable { return TEST_PROJECT1; @@ -250,7 +257,7 @@ public Project answer(InvocationOnMock invocation) throws Throwable { return TEST_PROJECT1; } }); - + // resource when(resourceRepo.findAllByDocumentProjectNameAndDocumentName(any(String.class), any(String.class))).thenReturn(new ArrayList()); diff --git a/src/test/java/edu/tamu/app/controller/DocumentControllerTest.java b/src/test/java/edu/tamu/app/controller/DocumentControllerTest.java index e3749a5a..d302cc80 100644 --- a/src/test/java/edu/tamu/app/controller/DocumentControllerTest.java +++ b/src/test/java/edu/tamu/app/controller/DocumentControllerTest.java @@ -48,6 +48,16 @@ public void testPushDocument() { Document document = (Document) response.getPayload().get("Document"); assertEquals(" The document has a different document name ", TEST_DOCUMENT1.getName(), document.getName()); assertEquals(" The document has a different project name ", TEST_PROJECT1.getName(), document.getProject().getName()); + assertEquals(" The document has isPublishing set to TRUE ", false, document.isPublishing()); + } + + @Test + public void testPushAlreadyPublishingDocument() { + response = documentController.push(TEST_DOCUMENT4.getProject().getName(), TEST_DOCUMENT4.getName()); + assertEquals(" The response did not error ", ApiStatus.ERROR, response.getMeta().getStatus()); + assertEquals(" The document did not error due to pending publications ", "Cannot publish because document is already pending publication", response.getMeta().getMessage()); + Document document = documentRepo.findOne(TEST_DOCUMENT4.getId()); + assertEquals(" The document has isPublishing set to FALSE ", true, document.isPublishing()); } @Test diff --git a/src/test/java/edu/tamu/app/controller/MockData.java b/src/test/java/edu/tamu/app/controller/MockData.java index 744008e6..4b93a261 100644 --- a/src/test/java/edu/tamu/app/controller/MockData.java +++ b/src/test/java/edu/tamu/app/controller/MockData.java @@ -111,15 +111,18 @@ public Project findProjectbyName(String projectName) { protected static Document TEST_DOCUMENT1 = new Document(TEST_PROJECT1, "Doc1 name", "documentPath1", "Unassigned"); protected static Document TEST_DOCUMENT2 = new Document(TEST_PROJECT1, "Doc2 name", "documentPath2", "Pending"); protected static Document TEST_DOCUMENT3 = new Document(TEST_PROJECT1, "Doc3 name", "documentPath3", "Accepted"); + protected static Document TEST_DOCUMENT4 = new Document(TEST_PROJECT1, "Doc4 name", "documentPath4", "Accepted"); static { TEST_DOCUMENT1.setId(1l); TEST_DOCUMENT2.setId(2l); TEST_DOCUMENT3.setId(3l); + TEST_DOCUMENT4.setId(4l); + TEST_DOCUMENT4.isPublishing(true); TEST_DOCUMENT1.setProject(TEST_PROJECT1); } - protected static List mockDocumentList = new ArrayList(Arrays.asList(new Document[] { TEST_DOCUMENT1, TEST_DOCUMENT2, TEST_DOCUMENT3 })); + protected static List mockDocumentList = new ArrayList(Arrays.asList(new Document[] { TEST_DOCUMENT1, TEST_DOCUMENT2, TEST_DOCUMENT3, TEST_DOCUMENT4 })); public Document saveDocument(Document modifiedDocument) { Document returnDocument = null; @@ -128,6 +131,7 @@ public Document saveDocument(Document modifiedDocument) { document.setProject(modifiedDocument.getProject()); document.setName(modifiedDocument.getName()); document.setStatus(modifiedDocument.getStatus()); + document.isPublishing(modifiedDocument.isPublishing()); returnDocument = document; break; } @@ -144,6 +148,15 @@ public Document findDocumentByProjectNameandName(String projectName, String docu return null; } + public Document findDocumentById(Long documentId) { + for (Document document: mockDocumentList) { + if (document.getId().equals(documentId)) { + return document; + } + } + return null; + } + // MetadataHeaders protected static List mockSpotlightExportedMetadataHeaders = new ArrayList(); protected static List> mockSpotlightMetdata = new ArrayList>();