/* * Copyright 2012 Research Studios Austria Forschungsges.m.b.H. * * 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 won.node.web; import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.NodeIterator; import org.apache.jena.rdf.model.Property; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.NoSuchMessageException; import org.springframework.http.*; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import org.springframework.util.StopWatch; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import won.cryptography.service.RegistrationServer; import won.node.service.impl.URIService; import won.protocol.exception.IncorrectPropertyCountException; import won.protocol.exception.NoSuchConnectionException; import won.protocol.exception.NoSuchNeedException; import won.protocol.exception.WonProtocolException; import won.protocol.message.WonMessageType; import won.protocol.model.DataWithEtag; import won.protocol.model.NeedState; import won.protocol.rest.WonEtagHelper; import won.protocol.service.LinkedDataService; import won.protocol.service.NeedInformationService; import won.protocol.util.RdfUtils; import won.protocol.vocabulary.CNT; import won.protocol.vocabulary.HTTP; import won.protocol.vocabulary.WONMSG; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * TODO: check the working draft here and see to conformance: * http://www.w3.org/TR/ldp/ * TODO: edit according to the latest version of the spec * Not met yet: * * 4.1.13 LDPR server responses must contain accurate response ETag header values. * * add dcterms:modified and dcterms:creator * * 4.4 HTTP PUT - we don't support that. especially: * 4.4.1 If HTTP PUT is performed ... (we do that using the owner protocol) * * 4.4.2 LDPR clients should use the HTTP If-Match header and HTTP ETags to ensure ... * * 4.5 HTTP DELETE - we don't support that. * * 4.6 HTTP HEAD - do we support that? * * 4.7 HTTP PATCH - we don't support that. * * 4.8 Common Properties - use common properties!! * * 5.1.2 Retrieving Only Non-member Properties - not supported (would have to be changed in LinkedDataServiceImpl * * see 5.3.2 LDPC - send 404 when non-member-properties is not supported... * * * 5.3.3 first page request: if a Request-URI of “<containerURL>?firstPage” is not supported --> 404 * * 5.3.4 support the firstPage query param * * 5.3.5 server initiated paging is a good idea (see 5.3.5.1 ) * * 5.3.7 ordering * */ @Controller @RequestMapping("/") public class LinkedDataWebController { final Logger logger = LoggerFactory.getLogger(getClass()); //full prefix of a need resource private String needResourceURIPrefix; //path of a need resource private String needResourceURIPath; //full prefix of a connection resource private String connectionResourceURIPrefix; //path of a connection resource private String connectionResourceURIPath; //prefix for URISs of RDF data private String dataURIPrefix; //prefix for URIs referring to real-world things private String resourceURIPrefix; //prefix for human readable pages private String pageURIPrefix; private String nodeResourceURIPrefix; @Autowired private LinkedDataService linkedDataService; @Autowired private RegistrationServer registrationServer; //date format for Expires header (rfc 1123) private static final String DATE_FORMAT_RFC_1123 = "EEE, dd MMM yyyy HH:mm:ss z"; //timeout for resources that clients may cache for a short term private static final int SHORT_TERM_CACHE_TIMEOUT_SECONDS = 600; @Autowired private URIService uriService; @RequestMapping(value="/", method = RequestMethod.GET) public String showIndexPage(){ return "index"; } //webmvc controller method @RequestMapping("${uri.path.page.need}/{identifier}") @Transactional(propagation = Propagation.REQUIRED) public String showNeedPage(@PathVariable String identifier, Model model, HttpServletResponse response) { try { URI needURI = uriService.createNeedURIForId(identifier); Dataset rdfDataset = linkedDataService.getNeedDataset(needURI); model.addAttribute("rdfDataset", rdfDataset); model.addAttribute("resourceURI", needURI.toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(needURI).toString()); return "rdfDatasetView"; } catch (NoSuchNeedException e) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return "notFoundView"; } } /** * This request URL should be protected by WebID filter because the result contains events data - which is data with * restricted access. See filterChainProxy in node-context.xml. * * @param identifier * @param model * @param response * @return */ //webmvc controller method @RequestMapping("${uri.path.page.need}/{identifier}/deep") @Transactional(propagation = Propagation.REQUIRED) public String showDeepNeedPage(@PathVariable String identifier, Model model, HttpServletResponse response, @RequestParam(value="layer-size", required=false) Integer layerSize) { try { URI needURI = uriService.createNeedURIForId(identifier); Dataset rdfDataset = linkedDataService.getNeedDataset(needURI, true, layerSize); model.addAttribute("rdfDataset", rdfDataset); model.addAttribute("resourceURI", needURI.toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(needURI).toString()); return "rdfDatasetView"; } catch (NoSuchNeedException|NoSuchConnectionException|NoSuchMessageException e) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return "notFoundView"; } } //webmvc controller method @RequestMapping("${uri.path.page.connection}/{identifier}") @Transactional(propagation = Propagation.REQUIRED) public String showConnectionPage(@PathVariable String identifier, Model model, HttpServletResponse response) { URI connectionURI = uriService.createConnectionURIForId(identifier); DataWithEtag<Dataset> rdfDataset = linkedDataService.getConnectionDataset(connectionURI, true, true, null); if (rdfDataset.isNotFound()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return "notFoundView"; } model.addAttribute("rdfDataset", rdfDataset.getData()); model.addAttribute("resourceURI", connectionURI.toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(connectionURI).toString()); return "rdfDatasetView"; } //webmvc controller method @RequestMapping("${uri.path.page.connection}/{identifier}/events") @Transactional(propagation = Propagation.REQUIRED) public String showConnectionEventsPage( @PathVariable String identifier, @RequestParam(value="p", required=false) Integer page, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="type", required=false) String type, @RequestParam(value="deep", required=false, defaultValue = "false") boolean deep, Model model, HttpServletResponse response) { try { URI connectionURI = uriService.createConnectionURIForId(identifier); String eventsURI = connectionURI.toString() + "/events"; Dataset rdfDataset = null; WonMessageType msgType = getMessageType(type); if (page == null && beforeId == null && afterId == null) { // all events, does not support type filtering for clients that do not support paging rdfDataset = linkedDataService.listConnectionEventURIs(connectionURI, deep); } else if (page != null) { // a page having particular page number is requested NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionEventURIs (connectionURI, page, null, msgType, deep); rdfDataset = resource.getContent(); } else if (beforeId != null) { // a page that precedes the item identified by the beforeId is requested URI referenceEvent = uriService.createEventURIForId(beforeId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionEventURIsBefore (connectionURI, referenceEvent, null, msgType, deep); rdfDataset = resource.getContent(); } else { // a page that follows the item identified by the afterId is requested URI referenceEvent = uriService.createEventURIForId(afterId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionEventURIsAfter (connectionURI, referenceEvent, null, msgType, deep); rdfDataset = resource.getContent(); } model.addAttribute("rdfDataset", rdfDataset); model.addAttribute("resourceURI", eventsURI); model.addAttribute("dataURI", uriService.toDataURIIfPossible(URI.create(eventsURI)).toString()); return "rdfDatasetView"; } catch (NoSuchConnectionException e) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return "notFoundView"; } } /** * This request URL should be protected by WebID filter because the result contains events data - which is data with * restricted access. See filterChainProxy in node-context.xml. * * @param identifier * @param model * @param response * @return */ //webmvc controller method @RequestMapping("${uri.path.page.event}/{identifier}") @Transactional(propagation = Propagation.REQUIRED) public String showEventPage(@PathVariable(value = "identifier") String identifier, Model model, HttpServletResponse response) { URI eventURI = uriService.createEventURIForId(identifier); DataWithEtag<Dataset> data = linkedDataService.getDatasetForUri(eventURI, null); if (model != null && ! data.isNotFound()) { model.addAttribute("rdfDataset", data.getData()); model.addAttribute("resourceURI", eventURI.toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(eventURI).toString()); return "rdfDatasetView"; } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return "notFoundView"; } } //webmvc controller method @RequestMapping("${uri.path.page.attachment}/{identifier}") @Transactional(propagation = Propagation.REQUIRED) public String showAttachmentPage(@PathVariable(value = "identifier") String identifier, Model model, HttpServletResponse response) { URI attachmentURI = uriService.createAttachmentURIForId(identifier); DataWithEtag<Dataset> data = linkedDataService.getDatasetForUri(attachmentURI, null); if (model != null && ! data.isNotFound()) { model.addAttribute("rdfDataset", data.getData()); model.addAttribute("resourceURI", attachmentURI.toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(attachmentURI).toString()); return "rdfDatasetView"; } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return "notFoundView"; } } //webmvc controller method @RequestMapping("${uri.path.page.need}") @Transactional(propagation = Propagation.REQUIRED) public String showNeedURIListPage( @RequestParam(value="p", required=false) Integer page, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="state", required=false) String state, HttpServletRequest request, Model model, HttpServletResponse response) throws IOException { Dataset rdfDataset = null; NeedState needState = getNeedState(state); if (page == null && beforeId == null && afterId == null) { //all needs, does not support need state filtering for clients that do not support paging rdfDataset = linkedDataService.listNeedURIs(); } else if (page != null) { NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listNeedURIs( page, null, needState); rdfDataset = resource.getContent(); } else if (beforeId != null) { URI referenceNeed = URI.create(this.needResourceURIPrefix + "/" + beforeId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listNeedURIsBefore( referenceNeed, null, needState); rdfDataset = resource.getContent(); } else { // afterId != null URI referenceNeed = URI.create(this.needResourceURIPrefix + "/" + afterId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listNeedURIsAfter( referenceNeed, null, needState); rdfDataset = resource.getContent(); } model.addAttribute("rdfDataset", rdfDataset); model .addAttribute("resourceURI", uriService.toResourceURIIfPossible(URI.create(request.getRequestURI())).toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(URI.create(request.getRequestURI())).toString()); return "rdfDatasetView"; } @RequestMapping("${uri.path.page}") @Transactional(propagation = Propagation.REQUIRED) public String showNodeInformationPage( HttpServletRequest request, Model model, HttpServletResponse response) { Dataset rdfDataset = linkedDataService.getNodeDataset(); model.addAttribute("rdfDataset", rdfDataset); model.addAttribute("resourceURI", uriService.toResourceURIIfPossible(URI.create(request.getRequestURI())).toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(URI.create(request.getRequestURI())).toString()); return "rdfDatasetView"; } //webmvc controller method @RequestMapping("${uri.path.page.connection}") @Transactional(propagation = Propagation.REQUIRED) public String showConnectionURIListPage( @RequestParam(value="p", required=false) Integer page, @RequestParam(value="deep", defaultValue = "false") boolean deep, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="timeof", required=false) String timestamp, HttpServletRequest request, Model model, HttpServletResponse response) { try { DateParameter dateParam = new DateParameter(timestamp); Dataset rdfDataset; if (page != null) { rdfDataset = linkedDataService.listConnectionURIs(page, null, dateParam.getDate(), deep).getContent(); } else if (beforeId != null) { URI connURI = uriService.createConnectionURIForId(beforeId); rdfDataset = linkedDataService.listConnectionURIsBefore(connURI, null, dateParam.getDate(), deep).getContent(); } else if (afterId != null) { URI connURI = uriService.createConnectionURIForId(afterId); rdfDataset = linkedDataService.listConnectionURIsAfter(connURI, null, dateParam.getDate(), deep).getContent(); } else { // all the connections; does not support date filtering for clients that do not support paging rdfDataset = linkedDataService.listConnectionURIs(deep); } model.addAttribute("rdfDataset", rdfDataset); model.addAttribute("resourceURI", uriService.toResourceURIIfPossible(URI.create(request.getRequestURI())).toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(URI.create(request.getRequestURI())).toString()); return "rdfDatasetView"; } catch (ParseException e) { model.addAttribute("error", "could not parse timestamp parameter"); return "notFoundView"; } catch (NoSuchConnectionException e) { model.addAttribute("error", "could not add connection data for " + e.getUnknownConnectionURI().toString()); return "notFoundView"; } } //webmvc controller method @RequestMapping("${uri.path.page.need}/{identifier}/connections") @Transactional(propagation = Propagation.REQUIRED) public String showConnectionURIListPage( @PathVariable String identifier, @RequestParam(value="p", required=false) Integer page, @RequestParam(value="deep",defaultValue = "false") boolean deep, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="type", required=false) String type, @RequestParam(value="timeof", required=false) String timestamp, HttpServletRequest request, Model model, HttpServletResponse response) { URI needURI = uriService.createNeedURIForId(identifier); try { DateParameter dateParam = new DateParameter(timestamp); WonMessageType eventsType = getMessageType(type); Dataset rdfDataset; if (page != null) { rdfDataset = linkedDataService.listConnectionURIs(page, needURI, null, eventsType, dateParam.getDate(), deep, true) .getContent(); } else if (beforeId != null) { URI connURI = uriService.createConnectionURIForId(beforeId); rdfDataset = linkedDataService.listConnectionURIsBefore( needURI, connURI, null, eventsType, dateParam.getDate(), deep, true).getContent(); } else if (afterId != null) { URI connURI = uriService.createConnectionURIForId(afterId); rdfDataset = linkedDataService.listConnectionURIsAfter( needURI, connURI, null, eventsType, dateParam.getDate(), deep, true).getContent(); } else { // all the connections of the need; does not support type and date filtering for clients that do not support // paging rdfDataset = linkedDataService.listConnectionURIs(needURI, deep, true); } model.addAttribute("rdfDataset", rdfDataset); model.addAttribute("resourceURI", uriService.toResourceURIIfPossible(URI.create(request.getRequestURI())).toString()); model.addAttribute("dataURI", uriService.toDataURIIfPossible(URI.create(request.getRequestURI())).toString()); return "rdfDatasetView"; } catch (ParseException e) { model.addAttribute("error", "could not parse timestamp parameter"); return "notFoundView"; } catch (NoSuchNeedException e) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return "notFoundView"; } catch (NoSuchConnectionException e) { logger.warn("did not find connection that should be connected to need. connection:{}", e.getUnknownConnectionURI()); return "notFoundView"; //TODO: should display an error view } } /** * If the HTTP 'Accept' header is an RDF MIME type * (as listed in the 'produces' value of the RequestMapping annotation), * a redirect to a data uri is sent. * @param request * @return */ @RequestMapping( value="${uri.path.resource}/**", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads", "*/*"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<String> redirectToData( HttpServletRequest request, HttpServletResponse response) throws IOException { URI resourceUriPrefix = URI.create(this.resourceURIPrefix); URI dataUri = URI.create(this.dataURIPrefix); String requestUri = getRequestUriWithQueryString(request); String redirectToURI = requestUri.replaceFirst(resourceUriPrefix.getPath(), dataUri.getPath()); logger.debug("resource URI requested with data mime type. redirecting from {} to {}", requestUri, redirectToURI); if (redirectToURI.equals(requestUri)) { logger.debug("redirecting to same URI avoided, sending status 500 instead"); return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR); } //TODO: actually the expiry information should be the same as that of the resource that is redirected to HttpHeaders headers = new HttpHeaders(); headers = addExpiresHeadersBasedOnRequestURI(headers, requestUri); //headers.setLocation(URI.create(redirectToURI)); addCORSHeader(headers); setResponseHeaders(response, headers); response.sendRedirect(redirectToURI); return null; } public void setResponseHeaders(final HttpServletResponse response, final HttpHeaders headers) { for(Map.Entry<String, List<String>> entry : headers.entrySet()){ for (String value : entry.getValue()) { response.setHeader(entry.getKey(), value); } } } private String getRequestUriWithQueryString(final HttpServletRequest request) { String requestUri = request.getRequestURI(); String queryString = request.getQueryString(); if (queryString != null){ requestUri += "?" + queryString; } return requestUri; } private String getRequestUriWithAddedQuery(final HttpServletRequest request, String query) { String requestUri = request.getRequestURI(); String queryString = request.getQueryString(); if (queryString == null || queryString.length() <= 2) { requestUri += "?" + query; } else { requestUri += "?" + queryString + "&" + query; } return requestUri; } /** * If the HTTP 'Accept' header is 'text/html' * (as listed in the 'produces' value of the RequestMapping annotation), * a redirect to a page uri is sent. * @param request * @return */ @RequestMapping( value="${uri.path.resource}/**", method = RequestMethod.GET, produces="text/html") @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<String> redirectToPage( HttpServletRequest request, HttpServletResponse response) throws IOException { URI resourceUriPrefix = URI.create(this.resourceURIPrefix); URI pageUriPrefix = URI.create(this.pageURIPrefix); String requestUri = getRequestUriWithQueryString(request); String redirectToURI = requestUri.replaceFirst(resourceUriPrefix.getPath(), pageUriPrefix.getPath()); logger.debug("resource URI requested with page mime type. redirecting from {} to {}", requestUri, redirectToURI); if (redirectToURI.equals(requestUri)) { logger.debug("redirecting to same URI avoided, sending status 500 instead"); return new ResponseEntity<String>("\"Could not redirect to linked data page\"", HttpStatus.INTERNAL_SERVER_ERROR); } //TODO: actually the expiry information should be the same as that of the resource that is redirected to HttpHeaders headers = new HttpHeaders(); headers = addExpiresHeadersBasedOnRequestURI(headers, requestUri); addCORSHeader(headers); //add a location header //headers.add("Location",redirectToURI); setResponseHeaders(response, headers); response.sendRedirect(redirectToURI); return null; } /** * If the request URI is the URI of a list page (list of needs, list of connections) it gets the * header that says 'already expired' so that crawlers re-download these data. For other URIs, the * 'never expires' header is added. * @param headers * @param requestUri * @return */ public HttpHeaders addExpiresHeadersBasedOnRequestURI(HttpHeaders headers, final String requestUri) { //now, we want to suppress the 'never expires' header information //for /resource/need and resource/connection so that crawlers always re-fetch these data URI requestUriAsURI = URI.create(requestUri); String requestPath = requestUriAsURI.getPath(); if (requestPath.replaceAll("/$","").endsWith(this.connectionResourceURIPath.replaceAll("/$", "")) || requestPath.replaceAll("/$","").endsWith(this.needResourceURIPath.replaceAll("/$", "")) ){ addMutableResourceHeaders(headers); } else { addImmutableResourceHeaders(headers); } return headers; } @RequestMapping( value="${uri.path.data.need}", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> listNeedURIs(HttpServletRequest request, HttpServletResponse response, @RequestParam(value="p", required=false) Integer page, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="state", required=false) String state) throws IOException { logger.debug("listNeedURIs() for page " + page + " called"); Dataset rdfDataset = null; HttpHeaders headers = new HttpHeaders(); Integer preferedSize = getPreferredSize(request); String passableQuery = getPassableQueryMap("state", state); NeedState needState = getNeedState(state); if (preferedSize == null) { // client doesn not support paging - return all needs; does not support need state filtering for clients that do // not support paging rdfDataset = linkedDataService.listNeedURIs(); } else if (page == null && beforeId == null && afterId == null) { // return latest needs NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listNeedURIs( 1, preferedSize, needState); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.needResourceURIPrefix), resource, passableQuery); // resume before parameter specified - display the connections with activities before the specified event id } else if (page != null) { NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listNeedURIs( page, preferedSize, needState); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.needResourceURIPrefix), resource, page, passableQuery); } else if (beforeId != null) { URI referenceNeed = URI.create(this.needResourceURIPrefix + "/" + beforeId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listNeedURIsBefore( referenceNeed, preferedSize, needState); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.needResourceURIPrefix), resource, passableQuery); } else { // afterId != null URI referenceNeed = URI.create(this.needResourceURIPrefix + "/" + afterId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listNeedURIsAfter( referenceNeed, preferedSize, needState); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.needResourceURIPrefix), resource, passableQuery); } addLocationHeaderIfNecessary(headers, URI.create(request.getRequestURI()), URI.create(this .needResourceURIPrefix)); addMutableResourceHeaders(headers); addCORSHeader(headers); return new ResponseEntity<Dataset>(rdfDataset, headers, HttpStatus.OK); } private NeedState getNeedState(final String state) { if (state != null) { return NeedState.parseString(state); } else { return null; } } private Integer getPreferredSize(final HttpServletRequest request) { Integer preferedSize = null; Enumeration<String> preferValue = request.getHeaders("Prefer"); if (preferValue != null) { //TODO share prefer pattern between methods, check the supported syntax according to HTTP protocol, and take // into account that client preference can also include max-triple-count and max-kbyte-count: Pattern pattern = Pattern.compile("(return=representation; max-member-count=)(\"?)([0-9]+)(\"?)"); while (preferValue.hasMoreElements() && preferedSize == null) { String value = preferValue.nextElement(); Matcher matcher = pattern.matcher(value); if (matcher.find()) { preferedSize = Integer.valueOf(matcher.group(3)); } } } return preferedSize; } @RequestMapping( value="${uri.path.data.connection}", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> listConnectionURIs( HttpServletRequest request, @RequestParam(value="p", required=false) Integer page, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="timeof", required=false) String timestamp, @RequestParam(value="deep", defaultValue = "false") boolean deep) { logger.debug("listConnectionURIs() called"); Dataset rdfDataset = null; HttpHeaders headers = new HttpHeaders(); Integer preferedSize = getPreferredSize(request); try { // even when the timestamp is not provided (null), we need to fix the time (if null, then to current), // because we will return prev/next links which make no sense if the time is not fixed DateParameter dateParam = new DateParameter(timestamp); String passableMap = getPassableQueryMap("timeof", dateParam.getTimestamp(), "deep", Boolean.toString(deep)); //if no preferred size provided by the client => the client does not support paging, return everything: if (preferedSize == null) { // all connections; does not support date filtering for clients that do not support paging rdfDataset = linkedDataService.listConnectionURIs(deep); } else if (page != null) { // return latest by the given timestamp NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionURIs( page, preferedSize, dateParam.getDate(), deep); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.connectionResourceURIPrefix), resource, page, passableMap); // resume before parameter specified - display the connections with activities before the specified event id } else if (beforeId == null && afterId == null) { // return latest by the given timestamp NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionURIs( 1, preferedSize, dateParam.getDate(), deep); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.connectionResourceURIPrefix), resource, passableMap); // resume before parameter specified - display the connections with activities before the specified event id } else { if (beforeId != null) { URI resumeConnURI = uriService.createConnectionURIForId(beforeId); NeedInformationService.PagedResource<Dataset, URI> resource = linkedDataService.listConnectionURIsBefore( resumeConnURI, preferedSize, dateParam.getDate(), deep); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.connectionResourceURIPrefix), resource, passableMap); // resume after parameter specified - display the connections with activities after the specified event id: } else { // if (afterId != null) URI resumeConnURI = uriService.createConnectionURIForId(afterId); NeedInformationService.PagedResource<Dataset, URI> resource = linkedDataService.listConnectionURIsAfter( resumeConnURI, preferedSize, dateParam.getDate(), deep); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, URI.create(this.connectionResourceURIPrefix), resource, passableMap); } } } catch (ParseException e) { logger.warn("could not parse timestamp into Date:{}", timestamp); return new ResponseEntity<Dataset>(HttpStatus.NOT_FOUND); } catch (NoSuchConnectionException e) { logger .warn("did not find connection that should be connected to need. connection:{}", e.getUnknownConnectionURI()); return new ResponseEntity<Dataset>(HttpStatus.INTERNAL_SERVER_ERROR); } addLocationHeaderIfNecessary(headers, URI.create(request.getRequestURI()), URI.create(this.connectionResourceURIPrefix)); addMutableResourceHeaders(headers); addCORSHeader(headers); return new ResponseEntity<Dataset>(rdfDataset, headers, HttpStatus.OK); } @RequestMapping( value="${uri.path.data.need}/{identifier}", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) public ResponseEntity<Dataset> readNeed( HttpServletRequest request, @PathVariable(value = "identifier") String identifier) { logger.debug("readNeed() called"); URI needUri = URI.create(this.needResourceURIPrefix + "/" + identifier); try { StopWatch stopWatch = new StopWatch(); stopWatch.start(); Dataset dataset = linkedDataService.getNeedDataset(needUri); //TODO: need information does change over time. The immutable need information should never expire, the mutable should HttpHeaders headers = new HttpHeaders(); addCORSHeader(headers); stopWatch.stop(); logger.debug("readNeed took " + stopWatch.getLastTaskTimeMillis() + " millis"); return new ResponseEntity<Dataset>(dataset, headers, HttpStatus.OK); } catch (NoSuchNeedException e) { return new ResponseEntity<Dataset>(HttpStatus.NOT_FOUND); } } /** * This request URL should be protected by WebID filter because the result contains events data - which is data with * restricted access. See filterChainProxy in node-context.xml. * * @param request * @param identifier * @return */ @RequestMapping( value="${uri.path.data.need}/{identifier}/deep", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> readNeedDeep( HttpServletRequest request, @PathVariable(value = "identifier") String identifier, @RequestParam(value="layer-size", required=false) Integer layerSize) { logger.debug("readNeed() called"); URI needUri = URI.create(this.needResourceURIPrefix + "/" + identifier); try { Dataset dataset = linkedDataService.getNeedDataset(needUri, true, layerSize); //TODO: need information does change over time. The immutable need information should never expire, the mutable should HttpHeaders headers = new HttpHeaders(); addCORSHeader(headers); return new ResponseEntity<Dataset>(dataset, headers, HttpStatus.OK); } catch (NoSuchNeedException|NoSuchConnectionException|NoSuchMessageException e) { return new ResponseEntity<Dataset>(HttpStatus.NOT_FOUND); } } @RequestMapping( value="${uri.path.data}", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> readNode( HttpServletRequest request) { logger.debug("readNode() called"); URI nodeUri = URI.create(this.nodeResourceURIPrefix); Dataset model = linkedDataService.getNodeDataset(); //TODO: need information does change over time. The immutable need information should never expire, the mutable should HttpHeaders headers = new HttpHeaders(); addCORSHeader(headers); addHeadersForShortTermCaching(headers); headers.add(HttpHeaders.CACHE_CONTROL, "public"); return new ResponseEntity<Dataset>(model, headers, HttpStatus.OK); } @RequestMapping( value="${uri.path.data.connection}/{identifier}", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> readConnection( HttpServletRequest request, @PathVariable(value="identifier") String identifier) { logger.debug("readConnection() called"); return getResponseEntity(identifier, request, new EtagSupportingDataLoader<Dataset>(){ @Override public URI createUriForIdentifier(final String identifier) { return URI.create(connectionResourceURIPrefix + "/" + identifier); } @Override public DataWithEtag<Dataset> loadDataWithEtag(final URI uri, final String etag) { return linkedDataService.getConnectionDataset(uri, true, true, etag); } @Override public void addHeaders(final HttpHeaders headers) { addCORSHeader(headers); addPublicHeaders(headers); addMutableResourceHeaders(headers); } }); } @RequestMapping( value="${uri.path.data.connection}/{identifier}/events", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> readConnectionEvents( HttpServletRequest request, @PathVariable(value="identifier") String identifier, @RequestParam(value="p", required=false) Integer page, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="type", required=false) String type, @RequestParam(value="deep", required = false, defaultValue = "false") boolean deep) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); logger.debug("readConnection() called"); Dataset rdfDataset = null; HttpHeaders headers = new HttpHeaders(); Integer preferedSize = getPreferredSize(request); URI connectionUri = URI.create(this.connectionResourceURIPrefix + "/" + identifier); URI connectionEventsURI = URI.create(connectionUri.toString() + "/" + "events"); WonMessageType msgType = getMessageType(type); try { String passableMap = getPassableQueryMap("type", type); if (preferedSize == null) { // client doesn't not support paging - return all members; does not support type filtering for clients that do // not support paging rdfDataset = linkedDataService.listConnectionEventURIs(connectionUri, deep); } else if (page == null && beforeId == null && afterId == null) { // client supports paging but didn't specify which page to return - return page with latest events NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionEventURIs (connectionUri, 1, preferedSize, msgType, deep); //TODO: does not respect preferredSize if deep is used rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionEventsURI, resource, passableMap); } else if (page != null) { // a page having particular page number is requested NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionEventURIs (connectionUri, page, preferedSize, msgType, deep); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionEventsURI, resource, page, passableMap); } else if (beforeId != null) { // a page that precedes the item identified by the beforeId is requested URI referenceEvent = uriService.createEventURIForId(beforeId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionEventURIsBefore (connectionUri, referenceEvent, preferedSize, msgType, deep); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionEventsURI, resource, passableMap); } else { // a page that follows the item identified by the afterId is requested URI referenceEvent = uriService.createEventURIForId(afterId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionEventURIsAfter (connectionUri, referenceEvent, preferedSize, msgType, deep); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionEventsURI, resource, passableMap); } } catch (NoSuchConnectionException e) { return new ResponseEntity<Dataset>(HttpStatus.NOT_FOUND); } //TODO: events list information does change over time, unless the connection is closed and cannot be reopened. // The events list of immutable connection information should never expire, the mutable should addLocationHeaderIfNecessary(headers, URI.create(request.getRequestURI()), URI.create(this.connectionResourceURIPrefix)); addMutableResourceHeaders(headers); addCORSHeader(headers); stopWatch.stop(); logger.debug("readConnectionEvents took " + stopWatch.getLastTaskTimeMillis() + " millis"); return new ResponseEntity<Dataset>(rdfDataset, headers, HttpStatus.OK); } private WonMessageType getMessageType(final String type) { if (type != null) { return WonMessageType.valueOf(type); } else { return null; } } /** * This request URL should be protected by WebID filter because the result contains events data - which is data with * restricted access. See filterChainProxy in node-context.xml. * * @param request * @param identifier * @return */ @RequestMapping( value="${uri.path.data.event}/{identifier}", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> readEvent( @PathVariable(value = "identifier") String identifier, HttpServletRequest request, HttpServletResponse response) { // get etag from headers, extract version identifier logger.debug("readConnectionEvent() called"); return getResponseEntity(identifier, request, new EtagSupportingDataLoader<Dataset>() { @Override public URI createUriForIdentifier(final String identifier) { return uriService.createEventURIForId(identifier); } @Override public DataWithEtag<Dataset> loadDataWithEtag(final URI uri, final String etag) { return linkedDataService.getDatasetForUri(uri, etag); } @Override public void addHeaders(final HttpHeaders headers) { addCORSHeader(headers); addPrivateHeaders(headers); addImmutableResourceHeaders(headers); } }); } private <T> ResponseEntity<T> getResponseEntity(String identifier, final HttpServletRequest request, EtagSupportingDataLoader<T> loader) { HttpHeaders requestHeaders = getHttpHeaders(request); WonEtagHelper requestEtagHelper = WonEtagHelper.fromHeaderIfCompatibleWithAcceptHeader(requestHeaders); String versionIdentifier = WonEtagHelper.getVersionIdentifier(requestEtagHelper); // fetch the data if required logger.debug("using version identifier {}", versionIdentifier); URI entityUri = loader.createUriForIdentifier(identifier); DataWithEtag<T> dataWithEtag = loader.loadDataWithEtag(entityUri, versionIdentifier); // prepare the response headers HttpHeaders headers = new HttpHeaders(); loader.addHeaders(headers); // set the etag headers setEtagHeaderForResponse(headers, dataWithEtag, requestEtagHelper); //return the response return getResponseEntityForPossiblyNotModifiedResult(dataWithEtag, headers); } @RequestMapping( value="${uri.path.data.attachment}/{identifier}", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads", "*/*"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> readAttachment( HttpServletRequest request, @PathVariable(value = "identifier") String identifier) { logger.debug("readAttachment() called"); URI attachmentURI = uriService.createAttachmentURIForId(identifier); DataWithEtag<Dataset> data = linkedDataService.getDatasetForUri(attachmentURI, null); if (!data.isNotFound()) { HttpHeaders headers = new HttpHeaders(); addCORSHeader(headers); String mimeTypeOfResponse = RdfUtils.findFirst(data.getData(), new RdfUtils.ModelVisitor<String>() { @Override public String visit(org.apache.jena.rdf.model.Model model) { String content = getObjectOfPropertyAsString(model, CNT.BYTES); if (content == null) return null; String contentType = getObjectOfPropertyAsString(model, WONMSG.CONTENT_TYPE); return contentType; } }); if (mimeTypeOfResponse != null){ //we found a base64 encoded attachment, we obtained its contentType, so we set it as the //contentType of the response. Set<MediaType> producibleMediaTypes = new HashSet<>(); producibleMediaTypes.add(MediaType.valueOf(mimeTypeOfResponse)); request.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producibleMediaTypes); } return new ResponseEntity<Dataset>(data.getData(), headers, HttpStatus.OK); } else { return new ResponseEntity<Dataset>(HttpStatus.NOT_FOUND); } } private String getObjectOfPropertyAsString(org.apache.jena.rdf.model.Model model, Property property){ NodeIterator nodeIteratr = model.listObjectsOfProperty(property); if (!nodeIteratr.hasNext()) return null; String ret = nodeIteratr.next().asLiteral().getString(); if (nodeIteratr.hasNext()) { throw new IncorrectPropertyCountException("found more than one property of cnt:bytes", 1, 2); } return ret; } /** * Get the RDF for the connections of the specified need. * * @param request * @param identifier * @param deep If true, connection data is added to the model (not only connection URIs). Default: false. * @param page taken into account only if client supports paging; in that case the specified page is returned * @param beforeId taken into account only if client supports paging; in that case the page with connections URIs * that precede the connection having beforeId is returned * @param afterId taken into account only if client supports paging; in that case the page with connections URIs * that follow the connection having afterId are returned * @param type only connection events of the given type are considered when ordering returned connections. * Default: all event types. * @param timestamp only connection events that where created before the given time are considered when ordering * returned connections. Default: current time. * @return */ @RequestMapping( value="${uri.path.data.need}/{identifier}/connections", method = RequestMethod.GET, produces={"application/ld+json", "application/trig", "application/n-quads"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<Dataset> readConnectionsOfNeed( HttpServletRequest request, @PathVariable(value="identifier") String identifier, @RequestParam(value="deep",defaultValue = "false") boolean deep, @RequestParam(value="p", required=false) Integer page, @RequestParam(value="resumebefore", required=false) String beforeId, @RequestParam(value="resumeafter", required=false) String afterId, @RequestParam(value="type", required=false) String type, @RequestParam(value="timeof", required=false) String timestamp) { logger.debug("readConnectionsOfNeed() called"); URI needUri = URI.create(this.needResourceURIPrefix + "/" + identifier); Dataset rdfDataset = null; HttpHeaders headers = new HttpHeaders(); Integer preferedSize = getPreferredSize(request); URI connectionsURI = URI.create(needUri.toString() + "/connections"); try { WonMessageType eventsType = getMessageType(type); DateParameter dateParam = new DateParameter(timestamp); String passableQuery = getPassableQueryMap("type", type, "timeof", dateParam.getTimestamp(), "deep", Boolean.toString(deep)); //if no preferred size provided by the client => the client does not support paging, return everything: if (preferedSize == null) { //does not support date and type filtering for clients that do not support paging rdfDataset = linkedDataService.listConnectionURIs(needUri, deep, true); // if no page or resume parameter is specified, display the latest connections: } else if (page == null && beforeId == null && afterId == null) { NeedInformationService.PagedResource<Dataset, URI> resource = linkedDataService.listConnectionURIs(1, needUri, preferedSize, eventsType, dateParam.getDate(), deep, true); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionsURI, resource, passableQuery); } else if (page != null) { NeedInformationService.PagedResource<Dataset, URI> resource = linkedDataService.listConnectionURIs(page, needUri, preferedSize, eventsType, dateParam.getDate(), deep, true); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionsURI, resource, page, passableQuery); } else { // resume before parameter specified - display the connections with activities before the specified event id: if (beforeId != null) { URI resumeConnURI = uriService.createConnectionURIForId(beforeId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionURIsBefore( needUri, resumeConnURI, preferedSize, eventsType, dateParam.getDate(), deep, true); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionsURI, resource, passableQuery); // resume after parameter specified - display the connections with activities after the specified event id: } else { // if (afterId != null) URI resumeConnURI = uriService.createConnectionURIForId(afterId); NeedInformationService.PagedResource<Dataset,URI> resource = linkedDataService.listConnectionURIsAfter( needUri, resumeConnURI, preferedSize, eventsType, dateParam.getDate(), deep, true); rdfDataset = resource.getContent(); addPagedResourceInSequenceHeader(headers, connectionsURI, resource, passableQuery); } } //append the required headers addMutableResourceHeaders(headers); addLocationHeaderIfNecessary(headers, URI.create(request.getRequestURI()), connectionsURI); addCORSHeader(headers); return new ResponseEntity<Dataset>(rdfDataset, headers, HttpStatus.OK); } catch (ParseException e) { logger.warn("could not parse timestamp into Date:{}", timestamp); return new ResponseEntity<Dataset>(HttpStatus.NOT_FOUND); } catch (NoSuchNeedException e) { logger.warn("did not find need {}", e.getUnknownNeedURI()); return new ResponseEntity<Dataset>(HttpStatus.NOT_FOUND); } catch (NoSuchConnectionException e) { logger .warn("did not find connection that should be connected to need. connection:{}", e.getUnknownConnectionURI()); return new ResponseEntity<Dataset>(HttpStatus.INTERNAL_SERVER_ERROR); } } /** * Checks if the actual URI is the same as the canonical URI; if not, adds a Location header to the response builder * indicating the canonical URI. * @param headers * @param actualURI * @param canonicalURI * @return the headers map with added header values */ private HttpHeaders addLocationHeaderIfNecessary(HttpHeaders headers, URI actualURI, URI canonicalURI){ if(!canonicalURI.resolve(actualURI).equals(canonicalURI)) { //the request URI is the canonical URI, it may be a DNS alias or relative //according to http://www.w3.org/TR/ldp/#general we have to include //the canonical URI in the lcoation header here headers.add(HTTP.HEADER_LOCATION, canonicalURI.toString()); } return headers; } /** * Adds headers describing the paged resource according to https://www.w3.org/TR/ldp-paging/ * (here implemented version is http://www.w3.org/TR/2015/NOTE-ldp-paging-20150630/) * that inform the client about the following properties of the pages resource: * * Link: <uri>; rel="canonical"; etag="tag" - which resource it is a page of, and current tag of the resource * Link: <http://www.w3.org/ns/ldp#Page>; rel="type" - that this is one in-sequence page resource * Link: <http://www.w3.org/ns/ldp#Resource>; rel="type" - that this is a LDP Resource (should be Container in our case?) * Link: <uri?p=x>; rel="next" - that the next in-sequence page resource exists and is retrievable at the given uri * * @param headers headers to which paged resource headers should be added * @param canonicalURI uri of the LDP Resource * @param page page of the Paged LDP Resource * @return the headers map with added header values */ private void addPagedResourceInSequenceHeader( final HttpHeaders headers, final URI canonicalURI, final NeedInformationService.PagedResource<Dataset,URI> resource, final int page, String queryPart) { headers.add("Link", "<http://www.w3.org/ns/ldp#Resource>; rel=\"type\", <http://www.w3.org/ns/ldp#Page>; rel=\"type\""); //Link: <http://example.org/customer-relations?p=2>; rel="next" if (resource.hasNext()) { int nextPage = page + 1; headers.add("Link", "<" + canonicalURI.toString() + "?p=" + nextPage + queryPart + ">; rel=\"next\""); } if (resource.hasPrevious() && page > 1) { int prevPage = page - 1; headers.add("Link", "<" + canonicalURI.toString() + "?p=" + prevPage + queryPart + ">; rel=\"prev\""); } headers.add("Link", "<" + canonicalURI.toString() + ">; rel=\"canonical\""); } private void addPagedResourceInSequenceHeader(final HttpHeaders headers, final URI canonicalURI, final NeedInformationService.PagedResource<Dataset,URI> resource, String queryPart) { headers.add("Link", "<http://www.w3.org/ns/ldp#Resource>; rel=\"type\", <http://www.w3.org/ns/ldp#Page>; rel=\"type\""); if (resource.hasNext()) { String id = extractResourceLocalId(resource.getResumeAfter()); headers.add("Link", "<" + canonicalURI.toString() + "?resumeafter=" + id + queryPart + ">; rel=\"next\""); } if (resource.hasPrevious()) { String id = extractResourceLocalId(resource.getResumeBefore()); headers.add("Link", "<" + canonicalURI.toString() + "?resumebefore=" + id + queryPart + ">; rel=\"prev\""); } headers.add("Link", "<" + canonicalURI.toString() + ">; rel=\"canonical\""); } private String getPassableQueryMap(String ... nameValue) { String queryPart = ""; for (int i = 0; i < nameValue.length; i++) { if (nameValue[i+1] != null) { queryPart = queryPart + "&" + nameValue[i] + "=" + nameValue[i+1]; } i++; } return queryPart; } private String extractResourceLocalId(final URI uri) { int startIdAfter = uri.toString().replaceAll("/$", "").lastIndexOf("/"); return uri.toString().substring(startIdAfter + 1); } /** * Headers: * types of resources * * public immutable * * public mutable * * private immutable * * private mutable * * public short-term cacheable * * privet short-term cacheable */ private void addPrivateHeaders(HttpHeaders headers){ headers.add(HttpHeaders.CACHE_CONTROL, "private"); // with no-store, the items don't survive a browser reload - but if the browser is closed, the items are gone //headers.add(HttpHeaders.CACHE_CONTROL, "no-store"); } private void addPublicHeaders(HttpHeaders headers){ headers.add(HttpHeaders.CACHE_CONTROL, "public"); } private void addImmutableResourceHeaders(HttpHeaders headers) { headers.add(HttpHeaders.CACHE_CONTROL, "max-age=31536000"); SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_RFC_1123, Locale.ENGLISH); headers.add(HTTP.HEADER_EXPIRES, dateFormat.format(getNeverExpiresDate())); headers.add(HTTP.HEADER_DATE, dateFormat.format(new Date())); } private void addMutableResourceHeaders(HttpHeaders headers) { headers.add(HttpHeaders.CACHE_CONTROL, "max-age=0"); headers.add(HttpHeaders.CACHE_CONTROL, "must-revalidate"); SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_RFC_1123, Locale.ENGLISH); headers.add(HTTP.HEADER_EXPIRES, "0"); headers.add(HTTP.HEADER_DATE, dateFormat.format(new Date())); } /** * Sets the Expires and Cache-Control header fields such that the response will be cached for a few minutes. * Useful for data that might change during a server reconfiguration but is otherwise quite stable. * @param headers * @return the headers map with added header values */ private HttpHeaders addHeadersForShortTermCaching(HttpHeaders headers){ SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_RFC_1123, Locale.ENGLISH); Calendar cal = Calendar.getInstance(); cal.setTime(new Date()); cal.add(Calendar.SECOND, SHORT_TERM_CACHE_TIMEOUT_SECONDS); headers.add(HTTP.HEADER_EXPIRES, dateFormat.format(cal.getTime())); headers.add(HTTP.HEADER_DATE, dateFormat.format(new Date())); headers.add(HttpHeaders.CACHE_CONTROL, "max-age=" + SHORT_TERM_CACHE_TIMEOUT_SECONDS); return headers; } //Calculates a date that, according to http spec, means 'never expires' //See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html private Date getNeverExpiresDate(){ Calendar cal = Calendar.getInstance(); cal.setTime(new Date()); cal.set(Calendar.YEAR, cal.get(Calendar.YEAR) + 1); return cal.getTime(); } /** * Adds the CORS headers required for client side cross-site requests. * See http://www.w3.org/TR/cors/ * @param headers */ private void addCORSHeader(final HttpHeaders headers) { headers.add("Access-Control-Allow-Origin", "*"); } private HttpHeaders getHttpHeaders(final HttpServletResponse response) { ServletServerHttpResponse servletResponse = new ServletServerHttpResponse(response); return servletResponse.getHeaders(); } private HttpHeaders getHttpHeaders(final HttpServletRequest request) { ServletServerHttpRequest servletRequest = new ServletServerHttpRequest(request); return servletRequest.getHeaders(); } private <T> ResponseEntity<T> getResponseEntityForPossiblyNotModifiedResult(final DataWithEtag<T> datasetWithEtag, final HttpHeaders headers) { if (datasetWithEtag.isChanged()) { if (datasetWithEtag != null) { return new ResponseEntity<>(datasetWithEtag.getData(), headers, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } else { return new ResponseEntity<>(headers, HttpStatus.NOT_MODIFIED); } } /** * Depending on whether the data has changed, use the old etag or create a new one. * @param headers * @param datasetWithEtag * @param requestEtagHelper */ private <T> void setEtagHeaderForResponse(final HttpHeaders headers, final DataWithEtag<T> datasetWithEtag, final WonEtagHelper requestEtagHelper) { // check if the data has changed if (datasetWithEtag.isChanged()) { logger.debug("ETAG comparison shows that data has changed or no etag was present"); // data has changed: create a new etag and put it into the header WonEtagHelper responseEtagHelper = WonEtagHelper.forVersion(datasetWithEtag.getEtag()); if (responseEtagHelper != null) { WonEtagHelper.setEtagHeader(responseEtagHelper, headers); } } else { // data has not changed: use the old etag value for the response ETag header WonEtagHelper.setEtagHeader(requestEtagHelper,headers); } } public void setLinkedDataService(final LinkedDataService linkedDataService) { this.linkedDataService = linkedDataService; } public void setRegistrationServer(final RegistrationServer registrationServer) { this.registrationServer = registrationServer; } public void setUriService(final URIService uriService) { this.uriService = uriService; } public void setNeedResourceURIPrefix(String needResourceURIPrefix) { this.needResourceURIPrefix = needResourceURIPrefix; } public void setConnectionResourceURIPrefix(String connectionResourceURIPrefix) { this.connectionResourceURIPrefix = connectionResourceURIPrefix; } public void setDataURIPrefix(String dataURIPrefix) { this.dataURIPrefix = dataURIPrefix; } public void setResourceURIPrefix(final String resourceURIPrefix) { this.resourceURIPrefix = resourceURIPrefix; } public void setPageURIPrefix(final String pageURIPrefix) { this.pageURIPrefix = pageURIPrefix; } public String getNodeResourceURIPrefix() { return nodeResourceURIPrefix; } public void setNodeResourceURIPrefix(String nodeResourceURIPrefix) { this.nodeResourceURIPrefix = nodeResourceURIPrefix; } public void setNeedResourceURIPath(final String needResourceURIPath) { this.needResourceURIPath = needResourceURIPath; } public void setConnectionResourceURIPath(final String connectionResourceURIPath) { this.connectionResourceURIPath = connectionResourceURIPath; } @RequestMapping( value="${uri.path.resource}", method = RequestMethod.POST, produces={"text/plain"}) @Transactional(propagation = Propagation.REQUIRED) public ResponseEntity<String> register(@RequestParam("register") String registeredType, HttpServletRequest request) throws CertificateException, UnsupportedEncodingException { logger.debug("REGISTERING " + registeredType); String supportedTypesMsg = "Request parameter error; supported 'register' parameter values: 'owner', 'node'"; if (registeredType == null) { logger.info(supportedTypesMsg); return new ResponseEntity<String>(supportedTypesMsg, HttpStatus.BAD_REQUEST); } PreAuthenticatedAuthenticationToken authentication = (PreAuthenticatedAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); if (! (authentication instanceof PreAuthenticatedAuthenticationToken)) { throw new BadCredentialsException("Could not register: PreAuthenticatedAuthenticationToken expected"); } Object principal = authentication.getPrincipal(); Object credentials = authentication.getCredentials(); X509Certificate cert = null; if (credentials instanceof X509Certificate){ cert = (X509Certificate) credentials; } else { throw new BadCredentialsException("Could not register: expected to find a X509Certificate in the request"); } try { if (registeredType.equals("owner")) { String result = registrationServer.registerOwner(cert); logger.debug("successfully registered owner"); return new ResponseEntity<String>(result, HttpStatus.OK); } if (registeredType.equals("node")) { String result = registrationServer.registerNode(cert); logger.debug("successfully registered node"); return new ResponseEntity<String>(result, HttpStatus.OK); } else { logger.debug(supportedTypesMsg); return new ResponseEntity<String>(supportedTypesMsg, HttpStatus.BAD_REQUEST); } } catch (WonProtocolException e) { logger.info("Could not register " + registeredType, e); return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST); } } private class DateParameter { private String timestamp; private Date date; private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS"; /** * Creates date parameter from String timestamp, assumes timestamp format is "yyyy-MM-dd'T'HH:mm:ss.SSS". * If timestamp is null, the parameter is assigned current time value. * * @param timestamp */ public DateParameter(final String timestamp) throws ParseException { DateFormat format = new SimpleDateFormat(TIMESTAMP_FORMAT, Locale.ENGLISH); if (timestamp == null) { this.date = new Date(); this.timestamp = format.format(date); } else { this.date = format.parse(timestamp); this.timestamp = timestamp; } } /** * Creates date parameter from Date. * * * @param date */ public DateParameter(final Date date) { DateFormat format = new SimpleDateFormat(TIMESTAMP_FORMAT, Locale.ENGLISH); this.timestamp = format.format(date); this.date = date; } /** * Gets time from String timestamp. * * @return */ public Date getDate() { return date; } /** * Gets String timestamp. * * @return */ public String getTimestamp() { return timestamp; } } }