Autopsy  4.4
Graphical digital forensics platform for The Sleuth Kit and other tools.
ImageExtractor.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2017 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.modules.embeddedfileextractor;
20 
21 import java.io.File;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.lang.IllegalArgumentException;
25 import java.lang.IndexOutOfBoundsException;
26 import java.lang.NullPointerException;
27 import java.nio.file.Paths;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.logging.Level;
31 import org.apache.poi.POIXMLException;
32 import org.apache.poi.hwpf.usermodel.Picture;
33 import org.apache.poi.hslf.usermodel.HSLFPictureData;
34 import org.apache.poi.hslf.usermodel.HSLFSlideShow;
35 import org.apache.poi.hssf.record.RecordInputStream.LeftoverDataException;
36 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
37 import org.apache.poi.hwpf.HWPFDocument;
38 import org.apache.poi.hwpf.model.PicturesTable;
39 import org.apache.poi.sl.usermodel.PictureData.PictureType;
40 import org.apache.poi.ss.usermodel.Workbook;
41 import org.apache.poi.util.RecordFormatException;
42 import org.apache.poi.xslf.usermodel.XMLSlideShow;
43 import org.apache.poi.xslf.usermodel.XSLFPictureData;
44 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
45 import org.apache.poi.xwpf.usermodel.XWPFDocument;
46 import org.apache.poi.xwpf.usermodel.XWPFPictureData;
47 import org.openide.util.NbBundle;
55 import org.sleuthkit.datamodel.AbstractFile;
56 import org.sleuthkit.datamodel.EncodedFileOutputStream;
57 import org.sleuthkit.datamodel.ReadContentInputStream;
58 import org.sleuthkit.datamodel.TskCoreException;
59 import org.sleuthkit.datamodel.TskData;
60 
61 class ImageExtractor {
62 
63  private final FileManager fileManager;
64  private final IngestServices services;
65  private static final Logger logger = Logger.getLogger(ImageExtractor.class.getName());
66  private final IngestJobContext context;
67  private String parentFileName;
68  private final String UNKNOWN_NAME_PREFIX = "image_"; //NON-NLS
69  private final FileTypeDetector fileTypeDetector;
70 
71  private String moduleDirRelative;
72  private String moduleDirAbsolute;
73 
77  enum SupportedImageExtractionFormats {
78 
79  DOC("application/msword"), //NON-NLS
80  DOCX("application/vnd.openxmlformats-officedocument.wordprocessingml.document"), //NON-NLS
81  PPT("application/vnd.ms-powerpoint"), //NON-NLS
82  PPTX("application/vnd.openxmlformats-officedocument.presentationml.presentation"), //NON-NLS
83  XLS("application/vnd.ms-excel"), //NON-NLS
84  XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); //NON-NLS
85 
86  private final String mimeType;
87 
88  SupportedImageExtractionFormats(final String mimeType) {
89  this.mimeType = mimeType;
90  }
91 
92  @Override
93  public String toString() {
94  return this.mimeType;
95  }
96  // TODO Expand to support more formats
97  }
98  private SupportedImageExtractionFormats abstractFileExtractionFormat;
99 
100  ImageExtractor(IngestJobContext context, FileTypeDetector fileTypeDetector, String moduleDirRelative, String moduleDirAbsolute) {
101 
102  this.fileManager = Case.getCurrentCase().getServices().getFileManager();
103  this.services = IngestServices.getInstance();
104  this.context = context;
105  this.fileTypeDetector = fileTypeDetector;
106  this.moduleDirRelative = moduleDirRelative;
107  this.moduleDirAbsolute = moduleDirAbsolute;
108  }
109 
119  boolean isImageExtractionSupported(AbstractFile abstractFile) {
120  try {
121  String abstractFileMimeType = fileTypeDetector.getFileType(abstractFile);
122  for (SupportedImageExtractionFormats s : SupportedImageExtractionFormats.values()) {
123  if (s.toString().equals(abstractFileMimeType)) {
124  abstractFileExtractionFormat = s;
125  return true;
126  }
127  }
128  return false;
129  } catch (TskCoreException ex) {
130  logger.log(Level.SEVERE, "Error executing FileTypeDetector.getFileType()", ex); // NON-NLS
131  return false;
132  }
133  }
134 
144  void extractImage(AbstractFile abstractFile) {
145  //
146  // switchcase for different supported formats
147  // process abstractFile according to the format by calling appropriate methods.
148 
149  List<ExtractedImage> listOfExtractedImages = null;
150  List<AbstractFile> listOfExtractedImageAbstractFiles = null;
151  this.parentFileName = EmbeddedFileExtractorIngestModule.getUniqueName(abstractFile);
152  //check if already has derived files, skip
153  try {
154  if (abstractFile.hasChildren()) {
155  //check if local unpacked dir exists
156  if (new File(getOutputFolderPath(parentFileName)).exists()) {
157  logger.log(Level.INFO, "File already has been processed as it has children and local unpacked file, skipping: {0}", abstractFile.getName()); //NON-NLS
158  return;
159  }
160  }
161  } catch (TskCoreException e) {
162  logger.log(Level.SEVERE, String.format("Error checking if file already has been processed, skipping: %s", parentFileName), e); //NON-NLS
163  return;
164  }
165  switch (abstractFileExtractionFormat) {
166  case DOC:
167  listOfExtractedImages = extractImagesFromDoc(abstractFile);
168  break;
169  case DOCX:
170  listOfExtractedImages = extractImagesFromDocx(abstractFile);
171  break;
172  case PPT:
173  listOfExtractedImages = extractImagesFromPpt(abstractFile);
174  break;
175  case PPTX:
176  listOfExtractedImages = extractImagesFromPptx(abstractFile);
177  break;
178  case XLS:
179  listOfExtractedImages = extractImagesFromXls(abstractFile);
180  break;
181  case XLSX:
182  listOfExtractedImages = extractImagesFromXlsx(abstractFile);
183  break;
184  default:
185  break;
186  }
187 
188  if (listOfExtractedImages == null) {
189  return;
190  }
191  // the common task of adding abstractFile to derivedfiles is performed.
192  listOfExtractedImageAbstractFiles = new ArrayList<>();
193  for (ExtractedImage extractedImage : listOfExtractedImages) {
194  try {
195  listOfExtractedImageAbstractFiles.add(fileManager.addDerivedFile(extractedImage.getFileName(), extractedImage.getLocalPath(), extractedImage.getSize(),
196  extractedImage.getCtime(), extractedImage.getCrtime(), extractedImage.getAtime(), extractedImage.getAtime(),
197  true, abstractFile, null, EmbeddedFileExtractorModuleFactory.getModuleName(), null, null, TskData.EncodingType.XOR1));
198  } catch (TskCoreException ex) {
199  logger.log(Level.SEVERE, NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.extractImage.addToDB.exception.msg"), ex); //NON-NLS
200  }
201  }
202  if (!listOfExtractedImages.isEmpty()) {
203  services.fireModuleContentEvent(new ModuleContentEvent(abstractFile));
204  context.addFilesToJob(listOfExtractedImageAbstractFiles);
205  }
206  }
207 
216  private List<ExtractedImage> extractImagesFromDoc(AbstractFile af) {
217  List<Picture> listOfAllPictures;
218 
219  try {
220  HWPFDocument doc = new HWPFDocument(new ReadContentInputStream(af));
221  PicturesTable pictureTable = doc.getPicturesTable();
222  listOfAllPictures = pictureTable.getAllPictures();
223  } catch (IOException | IllegalArgumentException |
224  IndexOutOfBoundsException | NullPointerException ex) {
225  // IOException:
226  // Thrown when the document has issues being read.
227 
228  // IllegalArgumentException:
229  // This will catch OldFileFormatException, which is thrown when the
230  // document's format is Word 95 or older. Alternatively, this is
231  // thrown when attempting to load an RTF file as a DOC file.
232  // However, our code verifies the file format before ever running it
233  // through the ImageExtractor. This exception gets thrown in the
234  // "IN10-0137.E01" image regardless. The reason is unknown.
235 
236  // IndexOutOfBoundsException:
237  // NullPointerException:
238  // These get thrown in certain images. The reason is unknown. It is
239  // likely due to problems with the file formats that POI is poorly
240  // handling.
241 
242  return null;
243  } catch (Throwable ex) {
244  // instantiating POI containers throw RuntimeExceptions
245  logger.log(Level.SEVERE, NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.docContainer.init.err", af.getName()), ex); //NON-NLS
246  return null;
247  }
248 
249  String outputFolderPath;
250  if (listOfAllPictures.isEmpty()) {
251  return null;
252  } else {
253  outputFolderPath = getOutputFolderPath(this.parentFileName);
254  }
255  if (outputFolderPath == null) {
256  return null;
257  }
258  List<ExtractedImage> listOfExtractedImages = new ArrayList<>();
259  byte[] data = null;
260  for (Picture picture : listOfAllPictures) {
261  String fileName = picture.suggestFullFileName();
262  try {
263  data = picture.getContent();
264  } catch (Exception ex) {
265  return null;
266  }
267  writeExtractedImage(Paths.get(outputFolderPath, fileName).toString(), data);
268  // TODO Extract more info from the Picture viz ctime, crtime, atime, mtime
269  listOfExtractedImages.add(new ExtractedImage(fileName, getFileRelativePath(fileName), picture.getSize(), af));
270  }
271 
272  return listOfExtractedImages;
273  }
274 
283  private List<ExtractedImage> extractImagesFromDocx(AbstractFile af) {
284  List<XWPFPictureData> listOfAllPictures = null;
285 
286  try {
287  XWPFDocument docx = new XWPFDocument(new ReadContentInputStream(af));
288  listOfAllPictures = docx.getAllPictures();
289  } catch (POIXMLException | IOException ex) {
290  // POIXMLException:
291  // Thrown when document fails to load
292 
293  // IOException:
294  // Thrown when the document has issues being read.
295 
296  return null;
297  } catch (Throwable ex) {
298  // instantiating POI containers throw RuntimeExceptions
299  logger.log(Level.SEVERE, NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.docxContainer.init.err", af.getName()), ex); //NON-NLS
300  return null;
301  }
302 
303  // if no images are extracted from the PPT, return null, else initialize
304  // the output folder for image extraction.
305  String outputFolderPath;
306  if (listOfAllPictures.isEmpty()) {
307  return null;
308  } else {
309  outputFolderPath = getOutputFolderPath(this.parentFileName);
310  }
311  if (outputFolderPath == null) {
312  return null;
313  }
314  List<ExtractedImage> listOfExtractedImages = new ArrayList<>();
315  byte[] data = null;
316  for (XWPFPictureData xwpfPicture : listOfAllPictures) {
317  String fileName = xwpfPicture.getFileName();
318  try {
319  data = xwpfPicture.getData();
320  } catch (Exception ex) {
321  return null;
322  }
323  writeExtractedImage(Paths.get(outputFolderPath, fileName).toString(), data);
324  listOfExtractedImages.add(new ExtractedImage(fileName, getFileRelativePath(fileName), xwpfPicture.getData().length, af));
325  }
326  return listOfExtractedImages;
327  }
328 
337  private List<ExtractedImage> extractImagesFromPpt(AbstractFile af) {
338  List<HSLFPictureData> listOfAllPictures = null;
339 
340  try {
341  HSLFSlideShow ppt = new HSLFSlideShow(new ReadContentInputStream(af));
342  listOfAllPictures = ppt.getPictureData();
343  } catch (IOException | IllegalArgumentException |
344  IndexOutOfBoundsException ex) {
345  // IllegalArgumentException:
346  // This will catch OldFileFormatException, which is thrown when the
347  // document version is unsupported. The IllegalArgumentException may
348  // also get thrown for unknown reasons.
349 
350  // IOException:
351  // Thrown when the document has issues being read.
352 
353  // IndexOutOfBoundsException:
354  // This gets thrown in certain images. The reason is unknown. It is
355  // likely due to problems with the file formats that POI is poorly
356  // handling.
357 
358  return null;
359  } catch (Throwable ex) {
360  // instantiating POI containers throw RuntimeExceptions
361  logger.log(Level.SEVERE, NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.pptContainer.init.err", af.getName()), ex); //NON-NLS
362  return null;
363  }
364 
365  // if no images are extracted from the PPT, return null, else initialize
366  // the output folder for image extraction.
367  String outputFolderPath;
368  if (listOfAllPictures.isEmpty()) {
369  return null;
370  } else {
371  outputFolderPath = getOutputFolderPath(this.parentFileName);
372  }
373  if (outputFolderPath == null) {
374  return null;
375  }
376 
377  // extract the images to the above initialized outputFolder.
378  // extraction path - outputFolder/image_number.ext
379  int i = 0;
380  List<ExtractedImage> listOfExtractedImages = new ArrayList<>();
381  byte[] data = null;
382  for (HSLFPictureData pictureData : listOfAllPictures) {
383 
384  // Get image extension, generate image name, write image to the module
385  // output folder, add it to the listOfExtractedImageAbstractFiles
386  PictureType type = pictureData.getType();
387  String ext;
388  switch (type) {
389  case JPEG:
390  ext = ".jpg"; //NON-NLS
391  break;
392  case PNG:
393  ext = ".png"; //NON-NLS
394  break;
395  case WMF:
396  ext = ".wmf"; //NON-NLS
397  break;
398  case EMF:
399  ext = ".emf"; //NON-NLS
400  break;
401  case PICT:
402  ext = ".pict"; //NON-NLS
403  break;
404  default:
405  continue;
406  }
407  String imageName = UNKNOWN_NAME_PREFIX + i + ext; //NON-NLS
408  try {
409  data = pictureData.getData();
410  } catch (Exception ex) {
411  return null;
412  }
413  writeExtractedImage(Paths.get(outputFolderPath, imageName).toString(), data);
414  listOfExtractedImages.add(new ExtractedImage(imageName, getFileRelativePath(imageName), pictureData.getData().length, af));
415  i++;
416  }
417  return listOfExtractedImages;
418  }
419 
428  private List<ExtractedImage> extractImagesFromPptx(AbstractFile af) {
429  List<XSLFPictureData> listOfAllPictures = null;
430 
431  try {
432  XMLSlideShow pptx = new XMLSlideShow(new ReadContentInputStream(af));
433  listOfAllPictures = pptx.getPictureData();
434  } catch (POIXMLException | IOException ex) {
435  // POIXMLException:
436  // Thrown when document fails to load.
437 
438  // IOException:
439  // Thrown when the document has issues being read
440 
441  return null;
442  } catch (Throwable ex) {
443  // instantiating POI containers throw RuntimeExceptions
444  logger.log(Level.SEVERE, NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.pptxContainer.init.err", af.getName()), ex); //NON-NLS
445  return null;
446  }
447 
448  // if no images are extracted from the PPT, return null, else initialize
449  // the output folder for image extraction.
450  String outputFolderPath;
451  if (listOfAllPictures.isEmpty()) {
452  return null;
453  } else {
454  outputFolderPath = getOutputFolderPath(this.parentFileName);
455  }
456  if (outputFolderPath == null) {
457  return null;
458  }
459 
460  List<ExtractedImage> listOfExtractedImages = new ArrayList<>();
461  byte[] data = null;
462  for (XSLFPictureData xslsPicture : listOfAllPictures) {
463 
464  // get image file name, write it to the module outputFolder, and add
465  // it to the listOfExtractedImageAbstractFiles.
466  String fileName = xslsPicture.getFileName();
467  try {
468  data = xslsPicture.getData();
469  } catch (Exception ex) {
470  return null;
471  }
472  writeExtractedImage(Paths.get(outputFolderPath, fileName).toString(), data);
473  listOfExtractedImages.add(new ExtractedImage(fileName, getFileRelativePath(fileName), xslsPicture.getData().length, af));
474 
475  }
476 
477  return listOfExtractedImages;
478 
479  }
480 
489  private List<ExtractedImage> extractImagesFromXls(AbstractFile af) {
490  List<? extends org.apache.poi.ss.usermodel.PictureData> listOfAllPictures = null;
491 
492  try {
493  Workbook xls = new HSSFWorkbook(new ReadContentInputStream(af));
494  listOfAllPictures = xls.getAllPictures();
495  } catch (IOException | LeftoverDataException |
496  RecordFormatException | IllegalArgumentException |
497  IndexOutOfBoundsException ex) {
498  // IllegalArgumentException:
499  // This will catch OldFileFormatException, which is thrown when the
500  // document version is unsupported. The IllegalArgumentException may
501  // also get thrown for unknown reasons.
502 
503  // IOException:
504  // Thrown when the document has issues being read.
505 
506  // LeftoverDataException:
507  // This is thrown for poorly formatted files that have more data
508  // than expected.
509 
510  // RecordFormatException:
511  // This is thrown for poorly formatted files that have less data
512  // that expected.
513 
514  // IllegalArgumentException:
515  // IndexOutOfBoundsException:
516  // These get thrown in certain images. The reason is unknown. It is
517  // likely due to problems with the file formats that POI is poorly
518  // handling.
519 
520  return null;
521  } catch (Throwable ex) {
522  // instantiating POI containers throw RuntimeExceptions
523  logger.log(Level.SEVERE, String.format("%s%s", NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.xlsContainer.init.err", af.getName()), af.getName()), ex); //NON-NLS
524  return null;
525  }
526 
527  // if no images are extracted from the PPT, return null, else initialize
528  // the output folder for image extraction.
529  String outputFolderPath;
530  if (listOfAllPictures.isEmpty()) {
531  return null;
532  } else {
533  outputFolderPath = getOutputFolderPath(this.parentFileName);
534  }
535  if (outputFolderPath == null) {
536  return null;
537  }
538 
539  int i = 0;
540  List<ExtractedImage> listOfExtractedImages = new ArrayList<>();
541  byte[] data = null;
542  for (org.apache.poi.ss.usermodel.PictureData pictureData : listOfAllPictures) {
543  String imageName = UNKNOWN_NAME_PREFIX + i + "." + pictureData.suggestFileExtension(); //NON-NLS
544  try {
545  data = pictureData.getData();
546  } catch (Exception ex) {
547  return null;
548  }
549  writeExtractedImage(Paths.get(outputFolderPath, imageName).toString(), data);
550  listOfExtractedImages.add(new ExtractedImage(imageName, getFileRelativePath(imageName), pictureData.getData().length, af));
551  i++;
552  }
553  return listOfExtractedImages;
554 
555  }
556 
565  private List<ExtractedImage> extractImagesFromXlsx(AbstractFile af) {
566  List<? extends org.apache.poi.ss.usermodel.PictureData> listOfAllPictures = null;
567 
568  try {
569  Workbook xlsx = new XSSFWorkbook(new ReadContentInputStream(af));
570  listOfAllPictures = xlsx.getAllPictures();
571  } catch (POIXMLException | IOException ex) {
572  // POIXMLException:
573  // Thrown when document fails to load.
574 
575  // IOException:
576  // Thrown when the document has issues being read
577 
578  return null;
579  } catch (Throwable ex) {
580  // instantiating POI containers throw RuntimeExceptions
581  logger.log(Level.SEVERE, NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.xlsxContainer.init.err", af.getName()), ex); //NON-NLS
582  return null;
583  }
584 
585  // if no images are extracted from the PPT, return null, else initialize
586  // the output folder for image extraction.
587  String outputFolderPath;
588  if (listOfAllPictures.isEmpty()) {
589  return null;
590  } else {
591  outputFolderPath = getOutputFolderPath(this.parentFileName);
592  }
593  if (outputFolderPath == null) {
594  return null;
595  }
596 
597  int i = 0;
598  List<ExtractedImage> listOfExtractedImages = new ArrayList<>();
599  byte[] data = null;
600  for (org.apache.poi.ss.usermodel.PictureData pictureData : listOfAllPictures) {
601  String imageName = UNKNOWN_NAME_PREFIX + i + "." + pictureData.suggestFileExtension();
602  try {
603  data = pictureData.getData();
604  } catch (Exception ex) {
605  return null;
606  }
607  writeExtractedImage(Paths.get(outputFolderPath, imageName).toString(), data);
608  listOfExtractedImages.add(new ExtractedImage(imageName, getFileRelativePath(imageName), pictureData.getData().length, af));
609  i++;
610  }
611  return listOfExtractedImages;
612 
613  }
614 
622  private void writeExtractedImage(String outputPath, byte[] data) {
623  try (EncodedFileOutputStream fos = new EncodedFileOutputStream(new FileOutputStream(outputPath), TskData.EncodingType.XOR1)) {
624  fos.write(data);
625  } catch (IOException ex) {
626  logger.log(Level.WARNING, "Could not write to the provided location: " + outputPath, ex); //NON-NLS
627  }
628  }
629 
639  private String getOutputFolderPath(String parentFileName) {
640  String outputFolderPath = moduleDirAbsolute + File.separator + parentFileName;
641  File outputFilePath = new File(outputFolderPath);
642  if (!outputFilePath.exists()) {
643  try {
644  outputFilePath.mkdirs();
645  } catch (SecurityException ex) {
646  logger.log(Level.WARNING, NbBundle.getMessage(this.getClass(), "EmbeddedFileExtractorIngestModule.ImageExtractor.getOutputFolderPath.exception.msg", parentFileName), ex);
647  return null;
648  }
649  }
650  return outputFolderPath;
651  }
652 
662  private String getFileRelativePath(String fileName) {
663  // Used explicit FWD slashes to maintain DB consistency across operating systems.
664  return "/" + moduleDirRelative + "/" + this.parentFileName + "/" + fileName; //NON-NLS
665  }
666 
672  private static class ExtractedImage {
673  //String fileName, String localPath, long size, long ctime, long crtime,
674  //long atime, long mtime, boolean isFile, AbstractFile parentFile, String rederiveDetails, String toolName, String toolVersion, String otherDetails
675 
676  private final String fileName;
677  private final String localPath;
678  private final long size;
679  private final long ctime;
680  private final long crtime;
681  private final long atime;
682  private final long mtime;
683  private final AbstractFile parentFile;
684 
685  ExtractedImage(String fileName, String localPath, long size, AbstractFile parentFile) {
686  this(fileName, localPath, size, 0, 0, 0, 0, parentFile);
687  }
688 
689  ExtractedImage(String fileName, String localPath, long size, long ctime, long crtime, long atime, long mtime, AbstractFile parentFile) {
690  this.fileName = fileName;
691  this.localPath = localPath;
692  this.size = size;
693  this.ctime = ctime;
694  this.crtime = crtime;
695  this.atime = atime;
696  this.mtime = mtime;
697  this.parentFile = parentFile;
698  }
699 
700  public String getFileName() {
701  return fileName;
702  }
703 
704  public String getLocalPath() {
705  return localPath;
706  }
707 
708  public long getSize() {
709  return size;
710  }
711 
712  public long getCtime() {
713  return ctime;
714  }
715 
716  public long getCrtime() {
717  return crtime;
718  }
719 
720  public long getAtime() {
721  return atime;
722  }
723 
724  public long getMtime() {
725  return mtime;
726  }
727 
728  public AbstractFile getParentFile() {
729  return parentFile;
730  }
731  }
732 }

Copyright © 2012-2016 Basis Technology. Generated on: Tue Jun 13 2017
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.