Java tutorial
/******************************************************************************* * Copyright 2015 DANS - Data Archiving and Networked Services * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package nl.knaw.dans.dccd.rest; import java.io.IOException; import java.io.StringWriter; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; import org.tridas.schema.*; import nl.knaw.dans.common.lang.dataset.DatasetState; import nl.knaw.dans.common.lang.search.SearchResult; import nl.knaw.dans.common.lang.search.SortOrder; import nl.knaw.dans.common.lang.search.simple.SimpleField; import nl.knaw.dans.common.lang.search.simple.SimpleSearchRequest; import nl.knaw.dans.common.lang.search.simple.SimpleSortField; import nl.knaw.dans.common.lang.service.exceptions.ServiceException; import nl.knaw.dans.common.lang.util.Range; import nl.knaw.dans.common.solr.SolrUtil; import nl.knaw.dans.dccd.application.services.DataServiceException; import nl.knaw.dans.dccd.application.services.DccdDataService; import nl.knaw.dans.dccd.application.services.DccdSearchService; import nl.knaw.dans.dccd.application.services.SearchServiceException; import nl.knaw.dans.dccd.model.DccdAssociatedFileBinaryUnit; import nl.knaw.dans.dccd.model.DccdOriginalFileBinaryUnit; import nl.knaw.dans.dccd.model.DccdUser; import nl.knaw.dans.dccd.model.Project; import nl.knaw.dans.dccd.model.ProjectPermissionLevel; import nl.knaw.dans.dccd.model.DccdUser.Role; import nl.knaw.dans.dccd.model.ProjectPermissionMetadata; import nl.knaw.dans.dccd.model.UserPermission; import nl.knaw.dans.dccd.rest.tridas.TridasPermissionRestrictor; import nl.knaw.dans.dccd.rest.tridas.TridasRequestedLevelRestrictor; import nl.knaw.dans.dccd.rest.util.UrlConverter; import nl.knaw.dans.dccd.rest.util.XmlStringUtil; import nl.knaw.dans.dccd.search.DccdProjectSB; import nl.knaw.dans.dccd.search.DccdSB; import nl.knaw.dans.dccd.tridas.TridasNamespacePrefixMapper; /** * * @author paulboon * */ @Path("/project") public class ProjectResource extends AbstractProjectResource { /** * extra parameters for 'harvesting' clients. */ public static final String MODIFIED_FROM_PARAM = "modFrom"; public static final String MODIFIED_UNTIL_PARAM = "modUntil"; /** * Get the complete tridas file, * you need to be logged in and authorized for download! * It is using the data service instead of the search service to get all of the data * * @param id * The store ID * @return */ @GET @Path("/{sid}/tridas") public Response getProjectTridasBySid(@PathParam("sid") String id) { // TODO prevent injection, sid must be "dccd:<number>" // authenticate user DccdUser user = null; try { user = authenticate(); if (user == null) return Response.status(Status.UNAUTHORIZED).build(); } catch (ServiceException e1) { e1.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } // get the project try { Project project = DccdDataService.getService().getProject(id); TridasProject tridasProject = project.getTridas(); if (!project.isDownloadAllowed(user)) { //return Response.status(Status.UNAUTHORIZED).build(); if (!project.isViewingAllowed(user)) return Response.status(Status.UNAUTHORIZED).build(); // Filter it for 'partial' download; what would be visible! ProjectPermissionLevel level = project.getEffectivePermissionLevel(user); TridasPermissionRestrictor permissionRestrictor = new TridasPermissionRestrictor(); permissionRestrictor.restrictToPermitted(tridasProject, level); } // Get all tridas xml java.io.StringWriter sw = new StringWriter(); JAXBContext jaxbContext = null; // System.out.println("\n TRiDaS XML, non valid, but with the structure"); try { // can it find the schema, and why not part of the lib jaxbContext = JAXBContext.newInstance("org.tridas.schema"); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); // improve the namespace mapping marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new TridasNamespacePrefixMapper()); marshaller.marshal(tridasProject, sw); } catch (JAXBException e) { e.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } // Always XML, because its TRiDaS return Response.status(Status.OK).entity(sw.toString()).build(); } catch (DataServiceException e) { return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } /** * Note that for the API consistency this should be the reverse mapping of * AbstractProjectResource.MAP_PERMISSION_TO_ENTITYLEVEL */ @SuppressWarnings({ "serial" }) public static final Map<String, ProjectPermissionLevel> MAP_ENTITYLEVEL_TO_PERMISSION = Collections .unmodifiableMap(new HashMap<String, ProjectPermissionLevel>() { { put("project", ProjectPermissionLevel.PROJECT); put("object", ProjectPermissionLevel.OBJECT); put("element", ProjectPermissionLevel.ELEMENT); put("sample", ProjectPermissionLevel.SAMPLE); put("radius", ProjectPermissionLevel.RADIUS); put("series", ProjectPermissionLevel.SERIES); put("values", ProjectPermissionLevel.VALUES); } }); @GET @Path("/{sid}/tridas/{entityLevel}") public Response getProjectTridasBySidForLevel(@PathParam("sid") String id, @PathParam("entityLevel") String entityLevel) { if (!MAP_ENTITYLEVEL_TO_PERMISSION.containsKey(entityLevel)) { // we only support the strings from the map return Response.status(Status.NOT_FOUND).build(); } // authenticate user DccdUser user = null; try { user = authenticate(); if (user == null) return Response.status(Status.UNAUTHORIZED).build(); } catch (ServiceException e1) { e1.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } // get the project try { Project project = DccdDataService.getService().getProject(id); TridasProject tridasProject = project.getTridas(); if (!project.isDownloadAllowed(user)) { //return Response.status(Status.UNAUTHORIZED).build(); if (!project.isViewingAllowed(user)) return Response.status(Status.UNAUTHORIZED).build(); // Filter it for 'partial' download; what would be visible! // first remove unwanted (not requested) stuff // TEST //ProjectPermissionLevel requestedlevel = ProjectPermissionLevel.PROJECT; ProjectPermissionLevel requestedlevel = MAP_ENTITYLEVEL_TO_PERMISSION.get(entityLevel); TridasRequestedLevelRestrictor requestedRestrictor = new TridasRequestedLevelRestrictor(); requestedRestrictor.restrictToPermitted(tridasProject, requestedlevel); // Finally use permission, if we requested more than allowed ProjectPermissionLevel level = project.getEffectivePermissionLevel(user); if (!requestedlevel.isPermittedBy(level)) { TridasPermissionRestrictor permissionRestrictor = new TridasPermissionRestrictor(); permissionRestrictor.restrictToPermitted(tridasProject, level); } } // Get all tridas xml java.io.StringWriter sw = new StringWriter(); JAXBContext jaxbContext = null; // System.out.println("\n TRiDaS XML, non valid, but with the structure"); try { // can it find the schema, and why not part of the lib jaxbContext = JAXBContext.newInstance("org.tridas.schema"); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);// testing marshaller.marshal(tridasProject, sw); // NOTE namespace is ugly, I did fix that somewhere? } catch (JAXBException e) { e.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } // Always XML, because its TRiDaS return Response.status(Status.OK).entity(sw.toString()).build(); } catch (DataServiceException e) { return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } /** * Produce a list in xml with the filenames, so you can request a download * normally you need to be logged in and authorized for download! * * @param id * The store ID * @return A response containing the complete list of associated files */ @GET @Path("/{sid}/associated") public Response listAssociatedFilesByProjectSid(@PathParam("sid") String id) { // authenticate user DccdUser user = null; try { user = authenticate(); if (user == null) return Response.status(Status.UNAUTHORIZED).build(); } catch (ServiceException e1) { e1.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } try { Project project = DccdDataService.getService().getProject(id); // For listing download is not needed! //if (!project.isDownloadAllowed(user) ) { // return Response.status(Status.UNAUTHORIZED).build(); //} java.io.StringWriter sw = new StringWriter(); sw.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"); // XML instruction sw.append("<files>"); List<DccdAssociatedFileBinaryUnit> fileBinaryUnits = project.getAssociatedFileBinaryUnits(); for (DccdAssociatedFileBinaryUnit unit : fileBinaryUnits) { sw.append(getXMLElementString("file", unit.getFileName())); } sw.append("</files>"); return responseXmlOrJson(sw.toString()); } catch (DataServiceException e) { e.printStackTrace(); } return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } /** * normally you need to be logged in and authorized for download! * * @param id * The store ID * @param filename * The name of the file to retrieve/download * @return */ @GET @Path("/{sid}/associated/{filename}") public Response getAssociatedFilesByProjectSid(@PathParam("sid") String id, @PathParam("filename") String filename) { // authenticate user DccdUser user = null; try { user = authenticate(); if (user == null) return Response.status(Status.UNAUTHORIZED).build(); } catch (ServiceException e1) { e1.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } try { Project project = DccdDataService.getService().getProject(id); if (!project.isDownloadAllowed(user)) { return Response.status(Status.UNAUTHORIZED).build(); } DccdAssociatedFileBinaryUnit requestedUnit = null; List<DccdAssociatedFileBinaryUnit> fileBinaryUnits = project.getAssociatedFileBinaryUnits(); for (DccdAssociatedFileBinaryUnit unit : fileBinaryUnits) { if (unit.getFileName().contentEquals(filename)) { requestedUnit = unit; break; // Found! } } if (requestedUnit == null) { // not found return Response.status(Status.NOT_FOUND).build(); } else { // found String unitId = requestedUnit.getUnitId(); // get the url URL fileURL = DccdDataService.getService().getFileURL(project.getSid(), unitId); // NOTE we have all bytes in memory, maybe we can get circumvent it with streaming byte[] bytes = UrlConverter.toByteArray(fileURL); // The file bytes return Response.status(Status.OK).entity(bytes).build(); } } catch (DataServiceException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } /** * Produce a list in xml with the filenames, so you can request a download * normally you need to be logged in and authorized for download! * * @param id * The store ID * @return A response containing the complete list of original files */ @GET @Path("/{sid}/originalvalues") public Response listOriginalFilesByProjectSid(@PathParam("sid") String id) { // authenticate user DccdUser user = null; try { user = authenticate(); if (user == null) return Response.status(Status.UNAUTHORIZED).build(); } catch (ServiceException e1) { e1.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } try { Project project = DccdDataService.getService().getProject(id); // For listing download is not needed! //if (!project.isDownloadAllowed(user) ) { // return Response.status(Status.UNAUTHORIZED).build(); //} java.io.StringWriter sw = new StringWriter(); sw.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"); // XML instruction sw.append("<files>"); List<DccdOriginalFileBinaryUnit> fileBinaryUnits = project.getOriginalFileBinaryUnits(); for (DccdOriginalFileBinaryUnit unit : fileBinaryUnits) { sw.append(getXMLElementString("file", unit.getFileName())); } sw.append("</files>"); return responseXmlOrJson(sw.toString()); } catch (DataServiceException e) { e.printStackTrace(); } return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } /** * normally you need to be logged in and authorized for download! * * @param id * The store ID * @param filename * The name of the file to retrieve/download * @return */ @GET @Path("/{sid}/originalvalues/{filename}") public Response getOriginalFilesByProjectSid(@PathParam("sid") String id, @PathParam("filename") String filename) { // authenticate user DccdUser user = null; try { user = authenticate(); if (user == null) return Response.status(Status.UNAUTHORIZED).build(); } catch (ServiceException e1) { e1.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } try { Project project = DccdDataService.getService().getProject(id); if (!project.isDownloadAllowed(user)) { return Response.status(Status.UNAUTHORIZED).build(); } DccdOriginalFileBinaryUnit requestedUnit = null; List<DccdOriginalFileBinaryUnit> fileBinaryUnits = project.getOriginalFileBinaryUnits(); for (DccdOriginalFileBinaryUnit unit : fileBinaryUnits) { if (unit.getFileName().contentEquals(filename)) { requestedUnit = unit; break; // Found! } } if (requestedUnit == null) { // not found return Response.status(Status.NOT_FOUND).build(); } else { // found String unitId = requestedUnit.getUnitId(); // get the url URL fileURL = DccdDataService.getService().getFileURL(project.getSid(), unitId); // NOTE we have all bytes in memory, maybe we can get circumvent it with streaming byte[] bytes = UrlConverter.toByteArray(fileURL); // The file bytes return Response.status(Status.OK).entity(bytes).build(); } } catch (DataServiceException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } @GET @Path("/{sid}/permission") public Response getPermission(@PathParam("sid") String id) { // authenticate user DccdUser user = null; try { user = authenticate(); if (user == null) return Response.status(Status.UNAUTHORIZED).build(); } catch (ServiceException e1) { e1.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } try { Project project = DccdDataService.getService().getProject(id); if (user.hasRole(Role.ADMIN) || user.getId().equals(project.getOwnerId())) { ProjectPermissionMetadata permissionMetadata = project.getPermissionMetadata(); java.io.StringWriter sw = new StringWriter(); sw.append(XmlStringUtil.XML_INSTRUCTION_STR); sw.append("<permission>"); sw.append(XmlStringUtil.getXMLElementString("projectId", project.getSid())); sw.append(XmlStringUtil.getXMLElementString("ownerId", project.getOwnerId())); sw.append(XmlStringUtil.getXMLElementString("defaultLevel", permissionMetadata.getDefaultLevel().toString())); ArrayList<UserPermission> userPermissionsArrayList = permissionMetadata .getUserPermissionsArrayList(); if (!userPermissionsArrayList.isEmpty()) { sw.append("<userPermissions>"); for (UserPermission userPermission : userPermissionsArrayList) { sw.append("<userPermission>"); sw.append(XmlStringUtil.getXMLElementString("userId", userPermission.getUserId())); sw.append(XmlStringUtil.getXMLElementString("level", userPermission.getLevel().toString())); sw.append("</userPermission>"); } sw.append("</userPermissions>"); } sw.append("</permission>"); return responseXmlOrJson(sw.toString()); } else { return Response.status(Status.UNAUTHORIZED).build(); } } catch (DataServiceException e) { e.printStackTrace(); } return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } /** * NOTE for searching with permission it might be needed to use the ObjectSB, * but we then get multiple results... * Project searching was in the GUI only on MyProjects! * We need to test it! * With ObjectSB only the tridas above the defaultlevel is indexed!!!! * Also note that advanced searching was always for logged-in users, never for anonymous ones. * Maybe use Path: project/object/query ? * * One solution might be to add a PublicProjectSB * and reindex with that one as well, filling it similar to the ObjectSB */ /** * List the projects with their information; * including sid's which can be used to get more data * * @param offset * @param limit * @return A response containing the paged list of Published/Archived projects */ @GET //@Path("/") public Response getProjects(@QueryParam(MODIFIED_FROM_PARAM) String modFromStr, @QueryParam(MODIFIED_UNTIL_PARAM) String modUntilStr, @QueryParam(OFFSET_PARAM) @DefaultValue("0") int offset, @QueryParam(LIMIT_PARAM) @DefaultValue("" + DEFAULT_LIST_LIMIT) int limit) { SearchResult<? extends DccdSB> searchResults = null; SimpleSearchRequest request = new SimpleSearchRequest(); request.setOffset(offset); // TODO support getting all results in a nice way // limit=0 means no-limit or 'give me everything' //if (limit == 0) //{ // // Solr has a default of 10 and no way to specify 'all' // limit = Integer.MAX_VALUE; //} request.setLimit(limit); // Show Project and not the standard Object result request.addFilterBean(DccdProjectSB.class); if (modFromStr != null || modUntilStr != null) { // Sorting on the date makes sense // Recently changed first (last archived) request.addSortField( new SimpleSortField(DccdProjectSB.ADMINISTRATIVE_STATE_LASTCHANGE, SortOrder.DESC)); try { addFilterQueryForModified(request, modFromStr, modUntilStr); } catch (IllegalArgumentException e) { return Response.status(Status.NOT_FOUND).build(); } } else { // sorting on the SID makes sense request.addSortField(new SimpleSortField(DccdProjectSB.PID_NAME, SortOrder.ASC)); } DccdUser requestingUser = null; try { requestingUser = authenticate(); } catch (ServiceException e1) { e1.printStackTrace(); } // Make sure it is published and not draft! SimpleField<String> stateField = new SimpleField<String>(DccdProjectSB.ADMINISTRATIVE_STATE_NAME, DatasetState.PUBLISHED.toString()); request.addFilterQuery(stateField); try { searchResults = DccdSearchService.getService().doSearch(request); return responseXmlOrJson(getProjectListSearchResultAsXml(searchResults, offset, limit, requestingUser)); } catch (SearchServiceException e) { e.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } private void addFilterQueryForModified(SimpleSearchRequest request, final String modFromStr, final String modUntilStr) throws IllegalArgumentException { DateTimeWrapper modFrom = null; DateTimeWrapper modUntil = null; // parse the strings an get DateTime objects // Note that it should be the same format as we use when outputting the "stateChanged" property. if (modFromStr != null) { DateTimeFormatter df = ISODateTimeFormat.dateTime(); modFrom = new DateTimeWrapper(df.parseDateTime(modFromStr)); } if (modUntilStr != null) { DateTimeFormatter df = ISODateTimeFormat.dateTime(); modUntil = new DateTimeWrapper(df.parseDateTime(modUntilStr)); } if (modFrom != null || modUntil != null) { // use DccdProjectSB.ADMINISTRATIVE_STATE_LASTCHANGE // Note that for Published (aka Archived) projects this is the timestamp for the publishing. // Draft projects can be modified without the 'change of state', but we won't expose those. SimpleField<Range<DateTimeWrapper>> periodField = new SimpleField<Range<DateTimeWrapper>>( DccdProjectSB.ADMINISTRATIVE_STATE_LASTCHANGE); periodField.setValue(new Range<DateTimeWrapper>(modFrom, modUntil)); request.addFilterQuery(periodField); } } // Wrapper to fix problems with DateTime within a Range query // that produces a wrong Solr query url // Should be fixed in 'dans-solr' commons project // nl.knaw.dans.common.solr.SolrUtil.toString(final Range<?> range) // public class DateTimeWrapper implements Comparable<DateTimeWrapper> { public DateTime d; public DateTimeWrapper(DateTime d) { this.d = d; } public String toString() { return SolrUtil.toString(d); } @Override public int compareTo(DateTimeWrapper o) { return d.compareTo(o.d); } } /** * Get the 'open access' project information, * what you see if you search without being logged in * Note that using search service is more efficient than going into the fedora archive/store * * @param id * The store ID * @return */ @GET @Path("/{sid}") public Response getProjectByStoreId(@PathParam("sid") String id) { SearchResult<? extends DccdSB> searchResults = null; SimpleSearchRequest request = new SimpleSearchRequest(); // one and only one result needed request.setLimit(1); request.setOffset(0); // Show Project and not the standard Object result request.addFilterBean(DccdProjectSB.class); request.addSortField(new SimpleSortField(DccdProjectSB.PID_NAME, SortOrder.ASC)); DccdUser requestingUser = null; try { requestingUser = authenticate(); } catch (ServiceException e1) { e1.printStackTrace(); } if (!isAdmin(requestingUser)) { // Make sure it is published and not draft! SimpleField<String> stateField = new SimpleField<String>(DccdProjectSB.ADMINISTRATIVE_STATE_NAME, DatasetState.PUBLISHED.toString()); request.addFilterQuery(stateField); } // restrict to specific sid SimpleField<String> idField = new SimpleField<String>(DccdProjectSB.PID_NAME, id); request.addFilterQuery(idField); try { searchResults = DccdSearchService.getService().doSearch(request); if (searchResults.getHits().isEmpty()) { return Response.status(Status.NOT_FOUND).build(); } else { DccdSB dccdSB = searchResults.getHits().get(0).getData(); return responseXmlOrJson(getProjectSearchResultAsXml(dccdSB, requestingUser)); } } catch (SearchServiceException e) { e.printStackTrace(); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } /** * Construct search result information as XML String * * @param dccdSB * search result */ private String getProjectSearchResultAsXml(DccdSB dccdSB, DccdUser requestingUser) { java.io.StringWriter sw = new StringWriter(); sw.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"); // XML instruction sw.append("<project>"); appendSearchResultDataAsXml(sw, dccdSB, requestingUser); sw.append("</project>"); return sw.toString(); } /** * Append project XML * * @param sw * writer to append to * @param dccdSB * search result */ protected void appendSearchResultDataAsXml(java.io.StringWriter sw, DccdSB dccdSB, DccdUser requestingUser) { appendProjectPublicDataAsXml(sw, dccdSB); appendProjectPublicLocationAsXml(sw, dccdSB); appendProjectPublicTimeRangeAsXml(sw, dccdSB); appendProjectPublicTaxonsAsXml(sw, dccdSB); appendProjectPublicTypesAsXml(sw, dccdSB); appendProjectPublicDescriptionAsXml(sw, dccdSB); // permission appendProjectPermissionAsXml(sw, dccdSB); if (isAdmin(requestingUser)) { sw.append(XmlStringUtil.getXMLElementString("state", dccdSB.getAdministrativeState().toString())); } } // TODO query and have the objects as result list, just like the GUI and a bit less than download search results // therefore we could have Object title and then the project info, nothing more // the problem is how to identify the objects.... }