/* * Autopsy Forensic Browser * * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * 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 org.sleuthkit.autopsy.casemodule; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.nio.file.Path; import java.nio.file.Paths; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.coreutils.XMLUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * Provides access to the case metadata stored in the case metadata file. */ public final class CaseMetadata { private static final String FILE_EXTENSION = ".aut"; private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)"); private static final String SCHEMA_VERSION_ONE = "1.0"; private final static String AUTOPSY_CREATED_VERSION_ELEMENT_NAME = "AutopsyCreatedVersion"; //NON-NLS private final static String CASE_DATABASE_NAME_ELEMENT_NAME = "DatabaseName"; //NON-NLS private final static String TEXT_INDEX_NAME_ELEMENT = "TextIndexName"; //NON-NLS private static final String CURRENT_SCHEMA_VERSION = "2.0"; private final static String ROOT_ELEMENT_NAME = "AutopsyCase"; //NON-NLS private final static String SCHEMA_VERSION_ELEMENT_NAME = "SchemaVersion"; //NON-NLS private final static String CREATED_DATE_ELEMENT_NAME = "CreatedDate"; //NON-NLS private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS private final static String AUTOPSY_CREATED_BY_ELEMENT_NAME = "CreatedByAutopsyVersion"; //NON-NLS private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS private final static String CASE_ELEMENT_NAME = "Case"; //NON-NLS private final static String CASE_NAME_ELEMENT_NAME = "Name"; //NON-NLS private final static String CASE_NUMBER_ELEMENT_NAME = "Number"; //NON-NLS private final static String EXAMINER_ELEMENT_NAME = "Examiner"; //NON-NLS private final static String CASE_TYPE_ELEMENT_NAME = "CaseType"; //NON-NLS private final static String CASE_DATABASE_ELEMENT_NAME = "Database"; //NON-NLS private final static String TEXT_INDEX_ELEMENT = "TextIndex"; //NON-NLS private final Path metadataFilePath; private Case.CaseType caseType; private String caseName; private String caseNumber; private String examiner; private String caseDatabase; private String textIndexName; private String createdDate; private String createdByVersion; /** * Gets the file extension used for case metadata files. * * @return The file extension. */ public static String getFileExtension() { return FILE_EXTENSION; } /** * Constructs an object that provides access to the case metadata stored in * a new case metadata file that is created using the supplied metadata. * * @param caseDirectory The case directory. * @param caseType The type of case. * @param caseName The name of the case. * @param caseNumber The case number. * @param examiner The name of the case examiner. * @param caseDatabase For a single-user case, the full path to the * case database file. For a multi-user case, the * case database name. * @param caseTextIndexName The text index name. * * @throws CaseMetadataException If the new case metadata file cannot be * created. */ CaseMetadata(String caseDirectory, Case.CaseType caseType, String caseName, String caseNumber, String examiner, String caseDatabase, String caseTextIndexName) throws CaseMetadataException { metadataFilePath = Paths.get(caseDirectory, caseName + FILE_EXTENSION); this.caseType = caseType; this.caseName = caseName; this.caseNumber = caseNumber; this.examiner = examiner; this.caseDatabase = caseDatabase; this.textIndexName = caseTextIndexName; createdByVersion = Version.getVersion(); createdDate = CaseMetadata.DATE_FORMAT.format(new Date()); writeToFile(); } /** * Constructs an object that provides access to the case metadata stored in * an existing case metadata file. * * @param metadataFilePath The full path to the case metadata file. * * @throws CaseMetadataException If the new case metadata file cannot be * read. */ public CaseMetadata(Path metadataFilePath) throws CaseMetadataException { this.metadataFilePath = metadataFilePath; readFromFile(); } /** * Gets the full path to the case metadata file. * * @return The path to the metadata file */ Path getFilePath() { return metadataFilePath; } /** * Gets the case directory. * * @return The case directory. */ public String getCaseDirectory() { return metadataFilePath.getParent().toString(); } /** * Gets the case type. * * @return The case type. */ public Case.CaseType getCaseType() { return caseType; } /** * Gets the case display name. * * @return The case display name. */ public String getCaseName() { return caseName; } /** * Sets the case display name. This does not change the name of the case * directory, the case database, or the text index name. * * @param caseName A case display name. */ void setCaseName(String caseName) throws CaseMetadataException { String oldCaseName = caseName; this.caseName = caseName; try { writeToFile(); } catch (CaseMetadataException ex) { this.caseName = oldCaseName; throw ex; } } /** * Gets the case number. * * @return The case number, may be empty. */ public String getCaseNumber() { return caseNumber; } /** * Gets the examiner. * * @return The examiner, may be empty. */ public String getExaminer() { return examiner; } /** * Gets the name of the case case database. * * @return The case database name. */ public String getCaseDatabaseName() { if (caseType == Case.CaseType.MULTI_USER_CASE) { return caseDatabase; } else { return Paths.get(caseDatabase).getFileName().toString(); } } /** * Gets the full path to the case database file if the case is a single-user * case. * * @return The full path to the case database file for a single-user case. * * @throws UnsupportedOperationException If called for a multi-user case. */ public String getCaseDatabasePath() throws UnsupportedOperationException { if (caseType == Case.CaseType.SINGLE_USER_CASE) { return caseDatabase; } else { throw new UnsupportedOperationException(); } } /** * Gets the text index name. * * @return The name of the text index for the case. */ public String getTextIndexName() { return textIndexName; } /** * Gets the date the case was created. * * @return The date this case was created as a string */ String getCreatedDate() { return createdDate; } /** * Sets the date the case was created. Used for preserving the case creation * date during single-user to multi-user case conversion. * * @param createdDate The date the case was created as a string. */ void setCreatedDate(String createdDate) throws CaseMetadataException { String oldCreatedDate = createdDate; this.createdDate = createdDate; try { writeToFile(); } catch (CaseMetadataException ex) { this.createdDate = oldCreatedDate; throw ex; } } /** * Gets the Autopsy version that created the case. * * @return A build identifier. */ String getCreatedByVersion() { return createdByVersion; } /** * Sets the Autopsy version that created the case. Used for preserving this * metadata during single-user to multi-user case conversion. * * @param buildVersion An build version identifier. */ void setCreatedByVersion(String buildVersion) throws CaseMetadataException { String oldCreatedByVersion = this.createdByVersion; this.createdByVersion = buildVersion; try { this.writeToFile(); } catch (CaseMetadataException ex) { this.createdByVersion = oldCreatedByVersion; throw ex; } } /** * Writes the case metadata to the metadata file. * * @throws CaseMetadataException If there is an error writing to the case * metadata file. */ private void writeToFile() throws CaseMetadataException { try { /* * Create the XML DOM. */ Document doc = XMLUtil.createDocument(); createXMLDOM(doc); doc.normalize(); /* * Prepare the DOM for pretty printing to the metadata file. */ Source source = new DOMSource(doc); StringWriter stringWriter = new StringWriter(); Result streamResult = new StreamResult(stringWriter); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //NON-NLS transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); //NON-NLS transformer.transform(source, streamResult); /* * Write the DOM to the metadata file. */ try (BufferedWriter fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(metadataFilePath.toFile())))) { fileWriter.write(stringWriter.toString()); fileWriter.flush(); } } catch (ParserConfigurationException | TransformerException | IOException ex) { throw new CaseMetadataException(String.format("Error writing to case metadata file %s", metadataFilePath), ex); } } /* * Creates an XML DOM from the case metadata. */ private void createXMLDOM(Document doc) { /* * Create the root element and its children. */ Element rootElement = doc.createElement(ROOT_ELEMENT_NAME); doc.appendChild(rootElement); createChildElement(doc, rootElement, SCHEMA_VERSION_ELEMENT_NAME, CURRENT_SCHEMA_VERSION); createChildElement(doc, rootElement, CREATED_DATE_ELEMENT_NAME, createdDate); createChildElement(doc, rootElement, MODIFIED_DATE_ELEMENT_NAME, DATE_FORMAT.format(new Date())); createChildElement(doc, rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, createdByVersion); createChildElement(doc, rootElement, AUTOPSY_SAVED_BY_ELEMENT_NAME, Version.getVersion()); Element caseElement = doc.createElement(CASE_ELEMENT_NAME); rootElement.appendChild(caseElement); /* * Create the children of the case element. */ createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, caseName); createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseNumber); createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, examiner); createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, caseType.toString()); createChildElement(doc, caseElement, CASE_DATABASE_ELEMENT_NAME, caseDatabase); createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, textIndexName); } /** * Creates an XML element for the case metadata XML DOM. * * @param doc The document. * @param parentElement The parent element of the element to be created. * @param elementName The name of the element to be created. * @param elementContent The text content of the element to be created, may * be empty. */ private void createChildElement(Document doc, Element parentElement, String elementName, String elementContent) { Element element = doc.createElement(elementName); element.appendChild(doc.createTextNode(elementContent)); parentElement.appendChild(element); } /** * Reads the case metadata from the metadata file. * * @throws CaseMetadataException If there is an error reading from the case * metadata file. */ private void readFromFile() throws CaseMetadataException { try { /* * Parse the file into an XML DOM and get the root element. */ DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = builder.parse(this.getFilePath().toFile()); doc.getDocumentElement().normalize(); Element rootElement = doc.getDocumentElement(); if (!rootElement.getNodeName().equals(ROOT_ELEMENT_NAME)) { throw new CaseMetadataException("Case metadata file corrupted"); } /* * Get the content of the relevant children of the root element. */ String schemaVersion = getElementTextContent(rootElement, SCHEMA_VERSION_ELEMENT_NAME, true); this.createdDate = getElementTextContent(rootElement, CREATED_DATE_ELEMENT_NAME, true); if (schemaVersion.equals(SCHEMA_VERSION_ONE)) { this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_CREATED_VERSION_ELEMENT_NAME, true); } else { this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, true); } /* * Get the content of the children of the case element. */ NodeList caseElements = doc.getElementsByTagName(CASE_ELEMENT_NAME); if (caseElements.getLength() == 0) { throw new CaseMetadataException("Case metadata file corrupted"); } Element caseElement = (Element) caseElements.item(0); this.caseName = getElementTextContent(caseElement, CASE_NAME_ELEMENT_NAME, true); this.caseNumber = getElementTextContent(caseElement, CASE_NUMBER_ELEMENT_NAME, false); this.examiner = getElementTextContent(caseElement, EXAMINER_ELEMENT_NAME, false); this.caseType = Case.CaseType.fromString(getElementTextContent(caseElement, CASE_TYPE_ELEMENT_NAME, true)); if (null == this.caseType) { throw new CaseMetadataException("Case metadata file corrupted"); } if (schemaVersion.equals(SCHEMA_VERSION_ONE)) { this.caseDatabase = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true); this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true); } else { this.caseDatabase = getElementTextContent(caseElement, CASE_DATABASE_ELEMENT_NAME, true); this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, true); } /* * Update the file to the current schema, if necessary. */ if (!schemaVersion.equals(CURRENT_SCHEMA_VERSION)) { writeToFile(); } } catch (ParserConfigurationException | SAXException | IOException ex) { throw new CaseMetadataException(String.format("Error reading from case metadata file %s", metadataFilePath), ex); } } /** * Gets the text content of an XML element. * * @param parentElement The parent element. * @param elementName The element name. * @param contentIsRequired Whether or not the content is required. * * @return The text content, may be empty if not required. * * @throws CaseMetadataException If the element is missing or content is * required and it is empty. */ private String getElementTextContent(Element parentElement, String elementName, boolean contentIsRequired) throws CaseMetadataException { NodeList elementsList = parentElement.getElementsByTagName(elementName); if (elementsList.getLength() == 0) { throw new CaseMetadataException(String.format("Missing %s element from case metadata file %s", elementName, metadataFilePath)); } String textContent = elementsList.item(0).getTextContent(); if (textContent.isEmpty() && contentIsRequired) { throw new CaseMetadataException(String.format("Empty %s element in case metadata file %s", elementName, metadataFilePath)); } return textContent; } /** * Exception thrown by the CaseMetadata class when there is a problem * accessing the metadata for a case. */ public final static class CaseMetadataException extends Exception { private static final long serialVersionUID = 1L; private CaseMetadataException(String message) { super(message); } private CaseMetadataException(String message, Throwable cause) { super(message, cause); } } }