package com.adobe.prefs.admin.app; import com.adobe.prefs.admin.core.NodeResource; import com.adobe.prefs.admin.core.Paths; import com.adobe.prefs.admin.core.PrefResource; import com.adobe.prefs.admin.core.UrlIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.hateoas.ResourceSupport; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; 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.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.prefs.BackingStoreException; import java.util.prefs.InvalidPreferencesFormatException; import java.util.prefs.Preferences; import static org.springframework.http.HttpStatus.*; @Controller @RequestMapping("/**") class PrefsController { protected final Logger logger = LoggerFactory.getLogger(getClass()); enum PreferencesRoot { usr(Preferences.userRoot()), sys(Preferences.systemRoot()); private final Preferences prefs; PreferencesRoot(final Preferences prefs) { this.prefs = prefs; } @Override public String toString() { return '/' + name(); } } @RequestMapping(value = "/", method = RequestMethod.GET) String redirect() { return "redirect:" + PreferencesRoot.sys.toString() + '/'; } @RequestMapping("/*.*") @ResponseStatus(NOT_FOUND) void notFound() {} @RequestMapping(value = "/", method = RequestMethod.POST, consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE}) @ResponseBody ResponseEntity<Void> importPreferences(InputStream in) throws IOException, InvalidPreferencesFormatException { logger.info("Importing preferences from file..."); Preferences.importPreferences(in); logger.info("Preferences import succeeded"); return seeOtherResponse("/"); } @RequestMapping(value = "/", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @ResponseBody ResponseEntity<Void> importPreferences(@RequestParam MultipartFile file) throws IOException, InvalidPreferencesFormatException { return importPreferences(file.getInputStream()); } @RequestMapping(value = "/{root}/**", method = RequestMethod.GET) @ResponseBody ResourceSupport getPreference(@PathVariable PreferencesRoot root, HttpServletRequest request) throws BackingStoreException { final PrefSpec prefSpec = new PrefSpec(root, request); if (!root.prefs.nodeExists(prefSpec.nodePath)) { throw new ResourceNotFoundException(prefSpec.nodePath); } final Preferences prefs = root.prefs.node(prefSpec.nodePath); if (prefSpec.key == null) { return new NodeResource(root.toString(), prefs); } else { if (! Arrays.asList(prefs.keys()).contains(prefSpec.key) ) { throw new ResourceNotFoundException(); } return new PrefResource(root.toString(), prefs, prefSpec.key, true); } } @RequestMapping(value = "/{root}/**", method = RequestMethod.PUT) @ResponseBody ResponseEntity<Void> setPreference(@PathVariable PreferencesRoot root, HttpServletRequest request, @RequestParam(required = false) String value) throws BackingStoreException { final PrefSpec prefSpec = new PrefSpec(root, request); Preferences prefs = root.prefs.node(prefSpec.nodePath); if (prefSpec.key != null) { if (value == null) { throw new NoValueException(prefSpec.key); } try { prefs.put(prefSpec.key, URLDecoder.decode(value, "UTF-8")); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("WTF-8", e); } } return seeOtherResponse(prefSpec); } @RequestMapping(value = "/{root}/**", method = RequestMethod.DELETE) ResponseEntity<Void> removePreference(@PathVariable PreferencesRoot root, HttpServletRequest request) throws BackingStoreException { final PrefSpec prefSpec = new PrefSpec(root, request); if (! root.prefs.nodeExists(prefSpec.nodePath) ) { throw new ResourceNotFoundException(prefSpec.nodePath); } Preferences prefs = root.prefs.node(prefSpec.nodePath); if (prefSpec.key != null) { if (! Arrays.asList(prefs.keys()).contains(prefSpec.key) ) { throw new ResourceNotFoundException(prefSpec.key); } prefs.remove(prefSpec.key); } else { prefs.removeNode(); } return seeOtherResponse(prefSpec); } @RequestMapping(value = "/{root}/**", method = RequestMethod.GET, params = "export", produces = MediaType.APPLICATION_XML_VALUE) void export(@PathVariable PreferencesRoot root, HttpServletRequest request, HttpServletResponse response, @RequestParam String[] export) throws BackingStoreException, IOException { final Set<String> exportOptions = new HashSet<>(Arrays.asList(export)); final PrefSpec prefSpec = new PrefSpec(root, request); if (!root.prefs.nodeExists(prefSpec.nodePath)) { throw new ResourceNotFoundException(prefSpec.nodePath); } Preferences prefs = root.prefs.node(prefSpec.nodePath); final boolean shallow = exportOptions.contains("shallow"); response.setContentType("application/xml"); if (exportOptions.contains("file")) { response.setHeader("Content-Disposition", String.format("attachment; filename=\"prefs_%s%s_%s.xml\"", prefs.isUserNode() ? "usr" : "sys", prefs.absolutePath().length() > 1 ? prefs.absolutePath().replace('/', '-') : "", shallow ? "shallow" : "deep")); } if (shallow) { prefs.exportNode(response.getOutputStream()); } else { prefs.exportSubtree(response.getOutputStream()); } } ResponseEntity<Void> seeOtherResponse(PrefSpec prefSpec) { return seeOtherResponse(Paths.parent(Paths.path(prefSpec.root.toString(), prefSpec.nodePath, prefSpec.key))); } ResponseEntity<Void> seeOtherResponse(String location) { HttpHeaders headers = new HttpHeaders(); headers.set("Location", location); return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); } @ExceptionHandler @ResponseStatus(BAD_REQUEST) public void badRequest(IllegalArgumentException e) { logger.warn(BAD_REQUEST.getReasonPhrase(), e); } @ExceptionHandler @ResponseStatus(NOT_FOUND) public void notFound(ResourceNotFoundException e) { logger.info(NOT_FOUND.getReasonPhrase(), e); } @ExceptionHandler @ResponseStatus(SERVICE_UNAVAILABLE) public void serviceUnavailable(IllegalStateException e) { logger.error(SERVICE_UNAVAILABLE.getReasonPhrase(), e); } @ExceptionHandler @ResponseStatus(NOT_IMPLEMENTED) public void notImplemented(UnsupportedOperationException e) { logger.error(NOT_IMPLEMENTED.getReasonPhrase(), e); } @ExceptionHandler @ResponseStatus(INTERNAL_SERVER_ERROR) public void internalServerError(Exception e) { logger.error(INTERNAL_SERVER_ERROR.getReasonPhrase(), e); } class PrefSpec { final PreferencesRoot root; final String nodePath; final String key; PrefSpec(final PreferencesRoot root, HttpServletRequest request) { final String requestPath = UrlIO.DECODER.apply(request.getRequestURI()); if (requestPath != null && requestPath.startsWith(root.toString())) { String path = requestPath.substring(root.toString().length()); if (path.isEmpty() || !path.startsWith("/")) { path = "/" + path; } final int lastSlash = path.lastIndexOf('/'); nodePath = path.substring(0, lastSlash); key = lastSlash == path.length() - 1 ? null : path.substring(lastSlash + 1, path.length()); } else { throw new IllegalArgumentException(); } this.root = root; } } }