Autopsy  4.20.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
CaseMetadata.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2019 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.casemodule;
20 
21 import java.io.BufferedWriter;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.io.StringWriter;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.text.DateFormat;
31 import java.text.SimpleDateFormat;
32 import java.util.ArrayList;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.Date;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40 import java.util.Map.Entry;
41 import java.util.stream.Collectors;
42 import javax.xml.parsers.DocumentBuilder;
43 import javax.xml.parsers.DocumentBuilderFactory;
44 import javax.xml.parsers.ParserConfigurationException;
45 import javax.xml.transform.OutputKeys;
46 import javax.xml.transform.Result;
47 import javax.xml.transform.Source;
48 import javax.xml.transform.Transformer;
49 import javax.xml.transform.TransformerException;
50 import javax.xml.transform.TransformerFactory;
51 import javax.xml.transform.dom.DOMSource;
52 import javax.xml.transform.stream.StreamResult;
53 import org.apache.commons.lang3.StringUtils;
54 import org.apache.commons.lang3.tuple.Pair;
55 import org.openide.util.Lookup;
58 import org.w3c.dom.Document;
59 import org.w3c.dom.Element;
60 import org.w3c.dom.Node;
61 import org.w3c.dom.NodeList;
62 import org.xml.sax.SAXException;
63 
67 public final class CaseMetadata {
68 
69  private static final String FILE_EXTENSION = ".aut";
70  private static final String DATE_FORMAT_STRING = "yyyy/MM/dd HH:mm:ss (z)";
71  private static final DateFormat DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US);
72 
73  /*
74  * Fields from schema version 1
75  */
76  private static final String SCHEMA_VERSION_ONE = "1.0";
77  private final static String ROOT_ELEMENT_NAME = "AutopsyCase"; //NON-NLS
78  private final static String SCHEMA_VERSION_ELEMENT_NAME = "SchemaVersion"; //NON-NLS
79  private final static String CREATED_DATE_ELEMENT_NAME = "CreatedDate"; //NON-NLS
80  private final static String AUTOPSY_VERSION_ELEMENT_NAME = "AutopsyCreatedVersion"; //NON-NLS
81  private final static String CASE_ELEMENT_NAME = "Case"; //NON-NLS
82  private final static String CASE_NAME_ELEMENT_NAME = "Name"; //NON-NLS
83  private final static String CASE_NUMBER_ELEMENT_NAME = "Number"; //NON-NLS
84  private final static String EXAMINER_ELEMENT_NAME = "Examiner"; //NON-NLS
85  private final static String CASE_TYPE_ELEMENT_NAME = "CaseType"; //NON-NLS
86  private final static String CASE_DATABASE_NAME_ELEMENT_NAME = "DatabaseName"; //NON-NLS
87  private final static String TEXT_INDEX_NAME_ELEMENT = "TextIndexName"; //NON-NLS
88 
89  /*
90  * Fields from schema version 2
91  */
92  private static final String SCHEMA_VERSION_TWO = "2.0";
93  private final static String AUTOPSY_CREATED_BY_ELEMENT_NAME = "CreatedByAutopsyVersion"; //NON-NLS
94  private final static String CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME = "Database"; //NON-NLS
95  private final static String TEXT_INDEX_ELEMENT = "TextIndex"; //NON-NLS
96 
97  /*
98  * Fields from schema version 3
99  */
100  private static final String SCHEMA_VERSION_THREE = "3.0";
101  private final static String CASE_DISPLAY_NAME_ELEMENT_NAME = "DisplayName"; //NON-NLS
102  private final static String CASE_DB_NAME_RELATIVE_ELEMENT_NAME = "CaseDatabase"; //NON-NLS
103 
104  /*
105  * Fields from schema version 4
106  */
107  private static final String SCHEMA_VERSION_FOUR = "4.0";
108  private final static String EXAMINER_ELEMENT_PHONE = "ExaminerPhone"; //NON-NLS
109  private final static String EXAMINER_ELEMENT_EMAIL = "ExaminerEmail"; //NON-NLS
110  private final static String CASE_ELEMENT_NOTES = "CaseNotes"; //NON-NLS
111 
112  /*
113  * Fields from schema version 5
114  */
115  private static final String SCHEMA_VERSION_FIVE = "5.0";
116  private final static String ORIGINAL_CASE_ELEMENT_NAME = "OriginalCase"; //NON-NLS
117 
118  /*
119  * Fields from schema version 6
120  */
121  private static final String SCHEMA_VERSION_SIX = "6.0";
122  private final static String CONTENT_PROVIDER_ELEMENT_NAME = "ContentProvider";
123  private final static String CONTENT_PROVIDER_NAME_ELEMENT_NAME = "Name";
124  private final static String CONTENT_PROVIDER_ARG_DEFAULT_KEY = "DEFAULT";
125 
126  /*
127  * Unread fields, regenerated on save.
128  */
129  private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS
130  private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS
131 
132  private final static String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_SIX;
133 
134  private final Path metadataFilePath;
136  private String caseName;
138  private String caseDatabaseName;
139  private String caseDatabasePath; // Legacy
140  private String textIndexName; // Legacy
141  private String createdDate;
142  private String createdByVersion;
143  private CaseMetadata originalMetadata = null; // For portable cases
144  private String contentProviderName;
145 
151  public static String getFileExtension() {
152  return FILE_EXTENSION;
153  }
154 
160  public static DateFormat getDateFormat() {
161  return new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US);
162  }
163 
174  CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails) {
175  this(caseType, caseDirectory, caseName, caseDetails, null);
176  }
177 
189  CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails, CaseMetadata originalMetadata) {
190  metadataFilePath = Paths.get(caseDirectory, caseDetails.getCaseDisplayName() + FILE_EXTENSION);
191  this.caseType = caseType;
192  this.caseName = caseName;
193  this.caseDetails = caseDetails;
194  caseDatabaseName = "";
195  caseDatabasePath = "";
196  textIndexName = "";
197  createdByVersion = Version.getVersion();
198  createdDate = CaseMetadata.DATE_FORMAT.format(new Date());
199  this.originalMetadata = originalMetadata;
200  this.contentProviderName = originalMetadata == null ? null : originalMetadata.contentProviderName;
201  }
202 
212  public CaseMetadata(Path metadataFilePath) throws CaseMetadataException {
213  this.metadataFilePath = metadataFilePath;
214  readFromFile();
215  }
216 
225  public static Path getCaseMetadataFilePath(Path directoryPath) {
226  final File[] files = directoryPath.toFile().listFiles();
227  if (files != null) {
228  for (File file : files) {
229  final String fileName = file.getName().toLowerCase();
230  if (fileName.endsWith(CaseMetadata.getFileExtension()) && file.isFile()) {
231  return file.toPath();
232  }
233  }
234  }
235  return null;
236  }
237 
242  public String getContentProviderName() {
243  return this.contentProviderName;
244  }
245 
251  public Path getFilePath() {
252  return metadataFilePath;
253  }
254 
260  public String getCaseDirectory() {
261  return metadataFilePath.getParent().toString();
262  }
263 
270  return caseType;
271  }
272 
278  public String getCaseName() {
279  return caseName;
280  }
281 
288  return caseDetails;
289  }
290 
296  public String getCaseDisplayName() {
297  return caseDetails.getCaseDisplayName();
298  }
299 
300  void setCaseDetails(CaseDetails newCaseDetails) throws CaseMetadataException {
301  CaseDetails oldCaseDetails = this.caseDetails;
302  this.caseDetails = newCaseDetails;
303  try {
304  writeToFile();
305  } catch (CaseMetadataException ex) {
306  this.caseDetails = oldCaseDetails;
307  throw ex;
308  }
309  }
310 
316  public String getCaseNumber() {
317  return caseDetails.getCaseNumber();
318  }
319 
325  public String getExaminer() {
326  return caseDetails.getExaminerName();
327  }
328 
329  public String getExaminerPhone() {
330  return caseDetails.getExaminerPhone();
331  }
332 
333  public String getExaminerEmail() {
334  return caseDetails.getExaminerEmail();
335  }
336 
337  public String getCaseNotes() {
338  return caseDetails.getCaseNotes();
339  }
340 
346  public String getCaseDatabaseName() {
347  return caseDatabaseName;
348  }
349 
357  void setCaseDatabaseName(String caseDatabaseName) throws CaseMetadataException {
358  String oldCaseDatabaseName = this.caseDatabaseName;
359  this.caseDatabaseName = caseDatabaseName;
360  try {
361  writeToFile();
362  } catch (CaseMetadataException ex) {
363  this.caseDatabaseName = oldCaseDatabaseName;
364  throw ex;
365  }
366  }
367 
374  public String getTextIndexName() {
375  return textIndexName;
376  }
377 
383  public String getCreatedDate() {
384  return createdDate;
385  }
386 
395  void setCreatedDate(String createdDate) throws CaseMetadataException {
396  String oldCreatedDate = createdDate;
397  this.createdDate = createdDate;
398  try {
399  writeToFile();
400  } catch (CaseMetadataException ex) {
401  this.createdDate = oldCreatedDate;
402  throw ex;
403  }
404  }
405 
411  String getCreatedByVersion() {
412  return createdByVersion;
413  }
414 
423  void setCreatedByVersion(String buildVersion) throws CaseMetadataException {
424  String oldCreatedByVersion = this.createdByVersion;
425  this.createdByVersion = buildVersion;
426  try {
427  writeToFile();
428  } catch (CaseMetadataException ex) {
429  this.createdByVersion = oldCreatedByVersion;
430  throw ex;
431  }
432  }
433 
440  void writeToFile() throws CaseMetadataException {
441  try {
442  /*
443  * Create the XML DOM.
444  */
445  Document doc = XMLUtil.createDocument();
446  createXMLDOM(doc);
447  doc.normalize();
448 
449  /*
450  * Prepare the DOM for pretty printing to the metadata file.
451  */
452  Source source = new DOMSource(doc);
453  StringWriter stringWriter = new StringWriter();
454  Result streamResult = new StreamResult(stringWriter);
455  Transformer transformer = TransformerFactory.newInstance().newTransformer();
456  transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //NON-NLS
457  transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); //NON-NLS
458  transformer.transform(source, streamResult);
459 
460  /*
461  * Write the DOM to the metadata file. Add UTF-8 Characterset so it writes to the file
462  * correctly for non-latin characters
463  */
464  try (BufferedWriter fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(metadataFilePath.toFile()), StandardCharsets.UTF_8))) {
465  fileWriter.write(stringWriter.toString());
466  fileWriter.flush();
467  }
468 
469  } catch (ParserConfigurationException | TransformerException | IOException ex) {
470  throw new CaseMetadataException(String.format("Error writing to case metadata file %s", metadataFilePath), ex);
471  }
472  }
473 
474  /*
475  * Creates an XML DOM from the case metadata.
476  */
477  private void createXMLDOM(Document doc) {
478  /*
479  * Create the root element and its children.
480  */
481  Element rootElement = doc.createElement(ROOT_ELEMENT_NAME);
482  doc.appendChild(rootElement);
483  createChildElement(doc, rootElement, SCHEMA_VERSION_ELEMENT_NAME, CURRENT_SCHEMA_VERSION);
484  createChildElement(doc, rootElement, CREATED_DATE_ELEMENT_NAME, createdDate);
485  createChildElement(doc, rootElement, MODIFIED_DATE_ELEMENT_NAME, DATE_FORMAT.format(new Date()));
486  createChildElement(doc, rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, createdByVersion);
487  createChildElement(doc, rootElement, AUTOPSY_SAVED_BY_ELEMENT_NAME, Version.getVersion());
488  Element caseElement = doc.createElement(CASE_ELEMENT_NAME);
489  rootElement.appendChild(caseElement);
490 
491  Element contentProviderEl = doc.createElement(CONTENT_PROVIDER_ELEMENT_NAME);
492  rootElement.appendChild(contentProviderEl);
493 
494  Element contentProviderNameEl = doc.createElement(CONTENT_PROVIDER_NAME_ELEMENT_NAME);
495  if (this.contentProviderName != null) {
496  contentProviderNameEl.setTextContent(this.contentProviderName);
497  }
498  contentProviderEl.appendChild(contentProviderNameEl);
499 
500  /*
501  * Create the children of the case element.
502  */
503  createCaseElements(doc, caseElement, this);
504 
505  /*
506  * Add original case element
507  */
508  Element originalCaseElement = doc.createElement(ORIGINAL_CASE_ELEMENT_NAME);
509  rootElement.appendChild(originalCaseElement);
510  if (originalMetadata != null) {
511  createChildElement(doc, originalCaseElement, CREATED_DATE_ELEMENT_NAME, originalMetadata.createdDate);
512  Element originalCaseDetailsElement = doc.createElement(CASE_ELEMENT_NAME);
513  originalCaseElement.appendChild(originalCaseDetailsElement);
514  createCaseElements(doc, originalCaseDetailsElement, originalMetadata);
515  }
516 
517  }
518 
526  private void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite) {
527  CaseDetails caseDetailsToWrite = metadataToWrite.caseDetails;
528  createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, metadataToWrite.caseName);
529  createChildElement(doc, caseElement, CASE_DISPLAY_NAME_ELEMENT_NAME, caseDetailsToWrite.getCaseDisplayName());
530  createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseDetailsToWrite.getCaseNumber());
531  createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, caseDetailsToWrite.getExaminerName());
532  createChildElement(doc, caseElement, EXAMINER_ELEMENT_PHONE, caseDetailsToWrite.getExaminerPhone());
533  createChildElement(doc, caseElement, EXAMINER_ELEMENT_EMAIL, caseDetailsToWrite.getExaminerEmail());
534  createChildElement(doc, caseElement, CASE_ELEMENT_NOTES, caseDetailsToWrite.getCaseNotes());
535  createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, metadataToWrite.caseType.toString());
536  createChildElement(doc, caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, metadataToWrite.caseDatabasePath);
537  createChildElement(doc, caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, metadataToWrite.caseDatabaseName);
538  createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, metadataToWrite.textIndexName);
539  }
540 
550  private void createChildElement(Document doc, Element parentElement, String elementName, String elementContent) {
551  Element element = doc.createElement(elementName);
552  element.appendChild(doc.createTextNode(elementContent));
553  parentElement.appendChild(element);
554  }
555 
562  private void readFromFile() throws CaseMetadataException {
563  try {
564  /*
565  * Parse the file into an XML DOM and get the root element.
566  */
567  DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
568  Document doc = builder.parse(this.getFilePath().toFile());
569  doc.getDocumentElement().normalize();
570  Element rootElement = doc.getDocumentElement();
571  if (!rootElement.getNodeName().equals(ROOT_ELEMENT_NAME)) {
572  throw new CaseMetadataException("Case metadata file corrupted");
573  }
574 
575  /*
576  * Get the content of the relevant children of the root element.
577  */
578  String schemaVersion = getElementTextContent(rootElement, SCHEMA_VERSION_ELEMENT_NAME, true);
579  this.createdDate = getElementTextContent(rootElement, CREATED_DATE_ELEMENT_NAME, true);
580  if (schemaVersion.equals(SCHEMA_VERSION_ONE)) {
581  this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_VERSION_ELEMENT_NAME, true);
582  } else {
583  this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, true);
584  }
585 
586  Element contentProviderEl = getChildElOrNull(rootElement, CONTENT_PROVIDER_ELEMENT_NAME);
587  if (contentProviderEl != null) {
588  Element contentProviderNameEl = getChildElOrNull(contentProviderEl, CONTENT_PROVIDER_NAME_ELEMENT_NAME);
589  this.contentProviderName = contentProviderNameEl != null ? contentProviderNameEl.getTextContent() : null;
590  } else {
591  this.contentProviderName = null;
592  }
593 
594  /*
595  * Get the content of the children of the case element.
596  */
597  NodeList caseElements = doc.getElementsByTagName(CASE_ELEMENT_NAME);
598  if (caseElements.getLength() == 0) {
599  throw new CaseMetadataException("Case metadata file corrupted");
600  }
601  Element caseElement = (Element) caseElements.item(0);
602  this.caseName = getElementTextContent(caseElement, CASE_NAME_ELEMENT_NAME, true);
603  String caseDisplayName;
604  String caseNumber;
605  if (schemaVersion.equals(SCHEMA_VERSION_ONE) || schemaVersion.equals(SCHEMA_VERSION_TWO)) {
606  caseDisplayName = caseName;
607  } else {
608  caseDisplayName = getElementTextContent(caseElement, CASE_DISPLAY_NAME_ELEMENT_NAME, true);
609  }
610  caseNumber = getElementTextContent(caseElement, CASE_NUMBER_ELEMENT_NAME, false);
611  String examinerName = getElementTextContent(caseElement, EXAMINER_ELEMENT_NAME, false);
612  String examinerPhone;
613  String examinerEmail;
614  String caseNotes;
615  if (schemaVersion.equals(SCHEMA_VERSION_ONE) || schemaVersion.equals(SCHEMA_VERSION_TWO) || schemaVersion.equals(SCHEMA_VERSION_THREE)) {
616  examinerPhone = ""; //case had metadata file written before additional examiner details were included
617  examinerEmail = "";
618  caseNotes = "";
619  } else {
620  examinerPhone = getElementTextContent(caseElement, EXAMINER_ELEMENT_PHONE, false);
621  examinerEmail = getElementTextContent(caseElement, EXAMINER_ELEMENT_EMAIL, false);
622  caseNotes = getElementTextContent(caseElement, CASE_ELEMENT_NOTES, false);
623  }
624 
625  this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, caseNotes);
626  this.caseType = Case.CaseType.fromString(getElementTextContent(caseElement, CASE_TYPE_ELEMENT_NAME, true));
627  if (null == this.caseType) {
628  throw new CaseMetadataException("Case metadata file corrupted");
629  }
630  switch (schemaVersion) {
631  case SCHEMA_VERSION_ONE:
632  this.caseDatabaseName = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true);
633  this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true);
634  break;
635  case SCHEMA_VERSION_TWO:
636  this.caseDatabaseName = getElementTextContent(caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, true);
637  this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
638  break;
639  default:
640  this.caseDatabaseName = getElementTextContent(caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, true);
641  this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
642  break;
643  }
644 
645  /*
646  * Fix up the case database name due to a bug that for a time caused
647  * the absolute paths of single-user case databases to be stored.
648  * Derive the missing (absolute/relative) value from the one
649  * present.
650  */
651  Path possibleAbsoluteCaseDbPath = Paths.get(this.caseDatabaseName);
652  Path caseDirectoryPath = Paths.get(getCaseDirectory());
653  if (possibleAbsoluteCaseDbPath.getNameCount() > 1) {
654  this.caseDatabasePath = this.caseDatabaseName;
655  this.caseDatabaseName = caseDirectoryPath.relativize(possibleAbsoluteCaseDbPath).toString();
656  } else {
657  this.caseDatabasePath = caseDirectoryPath.resolve(caseDatabaseName).toAbsolutePath().toString();
658  }
659 
660  } catch (ParserConfigurationException | SAXException | IOException ex) {
661  throw new CaseMetadataException(String.format("Error reading from case metadata file %s", metadataFilePath), ex);
662  }
663  }
664 
665  private Element getChildElOrNull(Element parent, String childTag) {
666  NodeList nl = parent.getElementsByTagName(childTag);
667  if (nl != null && nl.getLength() > 0 && nl.item(0) instanceof Element) {
668  return (Element) nl.item(0);
669  } else {
670  return null;
671  }
672  }
673 
686  private String getElementTextContent(Element parentElement, String elementName, boolean contentIsRequired) throws CaseMetadataException {
687  NodeList elementsList = parentElement.getElementsByTagName(elementName);
688  if (elementsList.getLength() == 0) {
689  throw new CaseMetadataException(String.format("Missing %s element from case metadata file %s", elementName, metadataFilePath));
690  }
691  String textContent = elementsList.item(0).getTextContent();
692  if (textContent.isEmpty() && contentIsRequired) {
693  throw new CaseMetadataException(String.format("Empty %s element in case metadata file %s", elementName, metadataFilePath));
694  }
695  return textContent;
696  }
697 
702  public final static class CaseMetadataException extends Exception {
703 
704  private static final long serialVersionUID = 1L;
705 
706  private CaseMetadataException(String message) {
707  super(message);
708  }
709 
710  private CaseMetadataException(String message, Throwable cause) {
711  super(message, cause);
712  }
713  }
714 
724  @Deprecated
725  public String getCaseDatabasePath() throws UnsupportedOperationException {
727  return Paths.get(getCaseDirectory(), caseDatabaseName).toString();
728  } else {
729  throw new UnsupportedOperationException();
730  }
731  }
732 
733 }
static CaseType fromString(String typeName)
Definition: Case.java:227
static Path getCaseMetadataFilePath(Path directoryPath)
Element getChildElOrNull(Element parent, String childTag)
void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite)
void createChildElement(Document doc, Element parentElement, String elementName, String elementContent)
String getElementTextContent(Element parentElement, String elementName, boolean contentIsRequired)

Copyright © 2012-2022 Basis Technology. Generated on: Tue Aug 1 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.