From 9866fa42e4adec094ffb317d002bf74c24213f38 Mon Sep 17 00:00:00 2001 From: Daniel Scalzi Date: Mon, 2 Mar 2020 19:13:29 -0500 Subject: [PATCH 1/2] Implement functionality to extract smgXml file from SMG, edit it, and repackage it. The input SMG file is not a zip archive, for some reason it is an SFX zip. Added a filter input stream to ignore the local headers, enabling us to read the file. The entries are cached and bundled into a fresh zip once the transformations are complete. --- .../controller/ResourceController.java | 79 ++++++++++++++---- .../service/SmgService.java | 83 +++++++++++++++++++ .../util/io/SfxZipInputStream.java | 46 ++++++++++ 3 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java create mode 100644 src/main/java/com/lmco/spectrum/systemnavigation3d/util/io/SfxZipInputStream.java diff --git a/src/main/java/com/lmco/spectrum/systemnavigation3d/controller/ResourceController.java b/src/main/java/com/lmco/spectrum/systemnavigation3d/controller/ResourceController.java index 3ab619b..6a2fc98 100644 --- a/src/main/java/com/lmco/spectrum/systemnavigation3d/controller/ResourceController.java +++ b/src/main/java/com/lmco/spectrum/systemnavigation3d/controller/ResourceController.java @@ -3,6 +3,7 @@ import com.amazonaws.services.s3.model.AmazonS3Exception; import com.amazonaws.util.IOUtils; import com.lmco.spectrum.systemnavigation3d.service.ResourceService; +import com.lmco.spectrum.systemnavigation3d.service.SmgService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -10,9 +11,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import java.io.IOException; import java.io.InputStream; @@ -21,36 +22,80 @@ @RequestMapping("api/resources") public class ResourceController { + private final HttpHeaders PDF_HEADERS; + @Autowired ResourceService resourceService; + @Autowired + SmgService smgService; - private ResponseEntity handleS3Input(String key, @Nullable HttpHeaders headers) { - try (InputStream is = resourceService.getFileAsStream(key)) { - return new ResponseEntity<>(IOUtils.toByteArray(is), headers, HttpStatus.OK); - } catch (AmazonS3Exception e) { - if(e.getErrorCode().equals("NoSuchKey")) { - return new ResponseEntity<>(e.getErrorMessage(), HttpStatus.NOT_FOUND); + public ResourceController() { + this.PDF_HEADERS = new HttpHeaders(); + PDF_HEADERS.setContentType(MediaType.APPLICATION_PDF); + } + + private HttpHeaders getAutoDownloadHeaders(String key) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentDispositionFormData(key, key); + return headers; + } + + private ResponseEntity respondToException(Exception e) { + if (e instanceof AmazonS3Exception) { + AmazonS3Exception ex = (AmazonS3Exception)e; + if (ex.getErrorCode().equals("NoSuchKey")) { + return new ResponseEntity<>(ex.getErrorMessage(), HttpStatus.NOT_FOUND); } else { - e.printStackTrace(); - return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + ex.printStackTrace(); + return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } - } catch (IOException e) { + } else if(e instanceof IOException) { e.printStackTrace(); return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } + + // Return 500 as default. + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } - @RequestMapping(value = "/pdf/{key}", method = RequestMethod.GET) + private ResponseEntity fetchResponseFromS3(String key, @Nullable HttpHeaders headers) { + try (InputStream is = resourceService.getFileAsStream(key)) { + return new ResponseEntity<>(IOUtils.toByteArray(is), headers, HttpStatus.OK); + } catch (Exception e) { + return respondToException(e); + } + } + + @GetMapping("/pdf/{key}") public ResponseEntity getResourcePDF(@PathVariable("key") String key){ - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_PDF); - // headers.setContentDispositionFormData(key, key); // Auto download - return handleS3Input(key, headers); + return fetchResponseFromS3(key, this.PDF_HEADERS); } - @RequestMapping(value = "/{key}", method = RequestMethod.GET) + @GetMapping("/{key}") public ResponseEntity getResource(@PathVariable("key") String key){ - return handleS3Input(key, null); + return fetchResponseFromS3(key, null); + } + + // TODO what input data? + @GetMapping("/smg/{key}") + public ResponseEntity getResourceSMG(@PathVariable("key") String key) { + + try(InputStream is = resourceService.getFileAsStream(String.format("smg/%s", key))) { + + byte[] data = IOUtils.toByteArray(is); + + byte[] processed = smgService.processSmg(data); + if(processed == null) { + // TODO failed to process, error? + // TODO maybe return error list + return new ResponseEntity<>("Failed to process SMG file.", HttpStatus.INTERNAL_SERVER_ERROR); + } else { + return new ResponseEntity<>(processed, getAutoDownloadHeaders(key), HttpStatus.OK); + } + + } catch(Exception e) { + return respondToException(e); + } } } diff --git a/src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java b/src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java new file mode 100644 index 0000000..0547e28 --- /dev/null +++ b/src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java @@ -0,0 +1,83 @@ +package com.lmco.spectrum.systemnavigation3d.service; + +import com.amazonaws.util.IOUtils; +import com.lmco.spectrum.systemnavigation3d.util.io.SfxZipInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +@Service +public class SmgService { + + private static final Logger log = LoggerFactory.getLogger(SmgService.class); + private static final String PRODUCT_XML = "product.smgXml"; + + // Max capacity is 2^31-1 bytes or ~2GB. + @Nullable + public byte[] processSmg(byte[] data){ + Map entryMap = getSmgXmlEntry(data); + byte[] smgXmlData = entryMap.get(PRODUCT_XML); + if(smgXmlData != null) { + String smgXml = new String(smgXmlData); + // TODO transform + + // Update XML entry. + entryMap.put(PRODUCT_XML, smgXml.getBytes()); + return repackageSmg(entryMap); + } else { + return null; + } + } + + public Map getSmgXmlEntry(byte[] data) { + + Map entryMap = new HashMap<>(); + + try(ZipInputStream zis = new ZipInputStream(new SfxZipInputStream(new ByteArrayInputStream(data)))) { + + ZipEntry entry; + while((entry = zis.getNextEntry()) != null) { + entryMap.put(entry.getName(), IOUtils.toByteArray(zis)); + zis.closeEntry(); + } + return entryMap; + + } catch (IOException e) { + log.error("Failed to read .smgXml entry!"); + e.printStackTrace(); + } + + return entryMap; + } + + @Nullable + public byte[] repackageSmg(Map entryMap) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(bos)) { + for(Map.Entry entry : entryMap.entrySet()) { + ZipEntry zipEntry = new ZipEntry(entry.getKey()); + zos.putNextEntry(zipEntry); + zos.write(entry.getValue()); + zos.closeEntry(); + } + } catch (IOException e) { + log.error("Failed to repackage smg file"); + e.printStackTrace(); + return null; + } + + return bos.toByteArray(); + + } + +} diff --git a/src/main/java/com/lmco/spectrum/systemnavigation3d/util/io/SfxZipInputStream.java b/src/main/java/com/lmco/spectrum/systemnavigation3d/util/io/SfxZipInputStream.java new file mode 100644 index 0000000..a27b0e5 --- /dev/null +++ b/src/main/java/com/lmco/spectrum/systemnavigation3d/util/io/SfxZipInputStream.java @@ -0,0 +1,46 @@ +package com.lmco.spectrum.systemnavigation3d.util.io; + +import javax.annotation.Nonnull; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +// Credit +// https://stackoverflow.com/questions/7924895/how-can-i-read-from-a-winzip-self-extracting-exe-zip-file-in-java +public class SfxZipInputStream extends FilterInputStream { + + // https://en.wikipedia.org/wiki/Zip_(file_format)#Local_file_header + public static final byte[] ZIP_LOCAL = { 0x50, 0x4b, 0x03, 0x04 }; + protected int ip; + protected int op; + + public SfxZipInputStream(InputStream is) { + super(is); + } + + @Override + public int read() throws IOException { + while(ip < ZIP_LOCAL.length) { + int c = super.read(); + if (c == ZIP_LOCAL[ip]) { + ip++; + } + else ip = 0; + } + + if (op < ZIP_LOCAL.length) + return ZIP_LOCAL[op++]; + else + return super.read(); + } + + @Override + public int read(@Nonnull byte[] b, int off, int len) throws IOException { + if (op == ZIP_LOCAL.length) return super.read(b, off, len); + int l = 0; + while (l < Math.min(len, ZIP_LOCAL.length)) { + b[l++] = (byte)read(); + } + return l; + } +} From bdc0eb56d986f02ab95c69bd9d17424f84c3f663 Mon Sep 17 00:00:00 2001 From: Daniel Scalzi Date: Mon, 2 Mar 2020 19:35:59 -0500 Subject: [PATCH 2/2] Add transformation service placeholder. --- .../systemnavigation3d/service/SmgService.java | 10 ++++++---- .../service/TransformationService.java | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/lmco/spectrum/systemnavigation3d/service/TransformationService.java diff --git a/src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java b/src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java index 0547e28..e1faf57 100644 --- a/src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java +++ b/src/main/java/com/lmco/spectrum/systemnavigation3d/service/SmgService.java @@ -4,6 +4,7 @@ import com.lmco.spectrum.systemnavigation3d.util.io.SfxZipInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.Nullable; @@ -22,6 +23,9 @@ public class SmgService { private static final Logger log = LoggerFactory.getLogger(SmgService.class); private static final String PRODUCT_XML = "product.smgXml"; + @Autowired + TransformationService transformationService; + // Max capacity is 2^31-1 bytes or ~2GB. @Nullable public byte[] processSmg(byte[] data){ @@ -29,10 +33,8 @@ public byte[] processSmg(byte[] data){ byte[] smgXmlData = entryMap.get(PRODUCT_XML); if(smgXmlData != null) { String smgXml = new String(smgXmlData); - // TODO transform - - // Update XML entry. - entryMap.put(PRODUCT_XML, smgXml.getBytes()); + smgXml = transformationService.transform(smgXml); + entryMap.put(PRODUCT_XML, smgXml.getBytes()); // Update XML entry. return repackageSmg(entryMap); } else { return null; diff --git a/src/main/java/com/lmco/spectrum/systemnavigation3d/service/TransformationService.java b/src/main/java/com/lmco/spectrum/systemnavigation3d/service/TransformationService.java new file mode 100644 index 0000000..e2d7b19 --- /dev/null +++ b/src/main/java/com/lmco/spectrum/systemnavigation3d/service/TransformationService.java @@ -0,0 +1,17 @@ +package com.lmco.spectrum.systemnavigation3d.service; + +import org.springframework.stereotype.Service; + +@Service +public class TransformationService { + + // TODO make functional + // TODO consider types of transformations we need to make + // accept list of transformation options? + // accept GEIA dtos and infer transformations? + // pending + public String transform(String smgXml){ + return smgXml; + } + +}