Java tutorial
/* * Copyright (C) 2004 by Tobias Buchloh. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the Free * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * If you didn't download this code from the following link, you should check if * you aren't using an obsolete version: * http://www.sourceforge.net/projects/KisKis */ package de.tbuchloh.kiskis.persistence; import static de.tbuchloh.kiskis.util.FileTools.listFiles; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.text.MessageFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.tbuchloh.kiskis.model.Attachment; import de.tbuchloh.kiskis.model.TPMDocument; import de.tbuchloh.kiskis.util.FileTools; import de.tbuchloh.kiskis.util.KisKisException; import de.tbuchloh.kiskis.util.KisKisRuntimeException; import de.tbuchloh.kiskis.util.Settings; import de.tbuchloh.util.StopWatch; import de.tbuchloh.util.crypto.CryptoException; import de.tbuchloh.util.crypto.CryptoInstallationTester; import de.tbuchloh.util.crypto.SymmetricAlgo; import de.tbuchloh.util.exceptions.ExceptionConverter; import de.tbuchloh.util.io.FileProcessor; import de.tbuchloh.util.localization.Messages; /** * <b>PersistenceManager</b>: * * @author gandalf * @version $Id$ */ public abstract class PersistenceManager { private static final class BackupFilter implements FilenameFilter { private final String _prefix; /** * creates a new BackupFilter * * @param file * is the original file */ public BackupFilter(final File file) { _prefix = file.getName() + BAK_ID; } /** * Overridden! * * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String) */ @Override public boolean accept(final File dir, final String name) { if (name.startsWith(_prefix)) { // there has to be at least one char after the last dot. return (name.lastIndexOf('.') + 1) < name.length(); } return false; } } public static final String ATTACHMENT_EXT = ".attachment."; protected static final SimpleDateFormat BAK_EXT = new SimpleDateFormat("yyyyMMddHHmmss"); private static final String BAK_ID = ".backup."; private static final MessageFormat ERR_ATT_NOT_EXISTS; private static final MessageFormat ERR_NOT_DECRYPTED; private static final MessageFormat ERR_UNKNOWN_TYPE; private static final Comparator<File> FILE_BY_DATE = new Comparator<File>() { @Override public int compare(final File lhs, final File rhs) { final Date lhsDate = getDate(lhs.getName()); final Date rhsDate = getDate(rhs.getName()); if (lhsDate.before(rhsDate)) { return -1; } return 1; } private String extractDate(final String name) { int offset = name.lastIndexOf(BAK_ID); offset += BAK_ID.length(); return name.substring(offset); } private Date getDate(final String name) { try { final String date = extractDate(name); assert date.length() == BAK_EXT.toPattern().length(); return BAK_EXT.parse(date); } catch (final ParseException e) { throw new Error("should never happen!", e); } } }; private static final boolean IS_DELETING_ORPHANS = false; private static final String K_MAX_BACKUPS = "maxBackups"; private static final Log LOG = LogFactory.getLog(PersistenceManager.class); private static int maxBackups; private static final MessageFormat ERR_ATT_DISAPPEARED; private static final Preferences P; private static final String PGP_HEADER = "-----BEGIN PGP MESSAGE-----"; private static final String XML_HEADER = "<?xml"; private static final MessageFormat ERR_UNKOWN_FILE; private static final Messages M = new Messages(PersistenceManager.class); static { ERR_UNKNOWN_TYPE = M.getFormat("ERR_UNKNOWN_TYPE"); //$NON-NLS-1$ ERR_NOT_DECRYPTED = M.getFormat("ERR_NOT_DECRYPTED"); //$NON-NLS-1$ ERR_ATT_NOT_EXISTS = M.getFormat("ERR_ATT_NOT_EXISTS"); //$NON-NLS-1$ ERR_ATT_DISAPPEARED = M.getFormat("ERR_ATT_DISAPPEARED"); //$NON-NLS-1$ ERR_UNKOWN_FILE = M.getFormat("ERR_UNKOWN_FILE"); //$NON-NLS-1$ P = Preferences.userNodeForPackage(PersistenceManager.class); maxBackups = P.getInt(K_MAX_BACKUPS, 5); } /** * @param docFile * the documents file * @param bakExt * the extension of the backup files * @throws IOException * if the attachment cannot be copied */ private static void backupAttachments(File docFile, String bakExt) throws IOException { assert docFile.exists(); for (final File f : FileTools.listFiles(docFile.getParentFile(), createAttachmentNamePattern(docFile))) { final File to = new File(f.getAbsolutePath() + bakExt); LOG.debug(String.format("Creating backup for attachment %1$s to %2$s", f.getName(), to.getName())); FileProcessor.copy(f, to); } } private static void checkAttachments(TPMDocument doc, IErrorHandler handler) { // check for missing attachment files for (final Attachment att : doc.getAttachments()) { final File attFile = createAttachmentFile(att); if (!attFile.isFile()) { handler.error("The attachment " + att.getId() + ", " + attFile + " (" + att.getDescription() + ") does not exist!"); } } // check for attachment files without any data representation for (final File f : getOrphanedAttachmentFiles(doc)) { handler.error("The file " + f + " is orphaned!"); if (isDeletingOrphans() && f.delete()) { handler.error(f + " DELETED!"); } } } public static FileFormats checkMimeType(final File file) throws PersistenceException { try { final String line = FileTools.readFirstLine(file); if (line == null) { LOG.debug("found empty file ..."); return FileFormats.UNKNOWN; } else if (line.startsWith(PGP_HEADER)) { LOG.debug("found PGP-file ..."); return FileFormats.PGP_FILE; } else if (line.startsWith(XML_HEADER)) { LOG.debug("found XML-file ... "); return FileFormats.XML_FILE; } else if (file.getName().endsWith("3des")) { LOG.debug("found TripeDES-file ... "); return FileFormats.TRIPLEDES_FILE; } LOG.debug("found unkown file line=" + line); return FileFormats.UNKNOWN; } catch (final IOException e) { throw new PersistenceException(e.getMessage(), e); } } private static void checkXmlHeader(final File file, final byte[] decrypted) throws PersistenceException { try { final String line = new String(decrypted, 0, Math.min(16, decrypted.length), "UTF-8"); if (!line.startsWith(XML_HEADER)) { final Object[] p = { file.getName(), line }; throw new PersistenceException(ERR_NOT_DECRYPTED.format(p)); } } catch (final UnsupportedEncodingException e) { throw new KisKisRuntimeException("Unsupported encoding!", e); } } private static void copyAttachments(final TPMDocument doc, final File dataFile) { for (final Attachment att : doc.getAttachments()) { try { copyAttachmentToDocument(att, dataFile); } catch (final IOException e) { LOG.error("Could not copy attachment " + att, e); } } } /** * @param att * is the attachment to be copied * @param document * is the document file which will own the attachments copied. * @throws IOException * if the attachment could not be copied. */ private static void copyAttachmentToDocument(Attachment att, File document) throws IOException { final File source = createAttachmentFile(att); final File target = createAttachmentFile(document.getAbsolutePath(), att.getId()); FileProcessor.copy(source, target); } /** * @param att * the attachment * @return the file to the attachment */ static File createAttachmentFile(Attachment att) { return createAttachmentFile(att.getDocument().getFile().getAbsolutePath(), att.getId()); } /** * @param documentFile * the path incl. file prefix * @param id * the attachment id * @return the file */ private static File createAttachmentFile(final String documentFile, final int id) { return new File(documentFile + ATTACHMENT_EXT + id); } /** * @param docFile * the document file * @return the pattern which attachment files need to fulfill */ private static String createAttachmentNamePattern(File docFile) { return Pattern.quote(docFile.getName()) + "\\.attachment\\.[0-9]+"; } private static void createBackup(final TPMDocument doc) throws IOException { if (doc.getFile().exists()) { deleteOldBackups(doc.getFile()); if (maxBackups > 0) { final String orig = doc.getFile().getAbsolutePath(); final String bakExt = BAK_ID + BAK_EXT.format(new Date()); final File bak = new File(orig + bakExt); LOG.debug("creating backup: " + bak.getName()); backupAttachments(doc.getFile(), bakExt); final File tmp = new File(doc.getFile().getAbsolutePath()); if (!tmp.renameTo(bak)) { throw new IOException(String.format("The file %1$s cannot be renamed to %2$s!", tmp, bak)); } assert bak.exists() && !doc.getFile().exists(); } } } /** * @param doc * the document */ private static void createNewAttachments(TPMDocument doc) throws IOException, CryptoException { final List<File> disappearedFiles = new ArrayList<File>(); for (final Attachment att : doc.getAttachments()) { if (att.getAttachedFile() != null) { final File target = createAttachmentFile(att); LOG.debug(String.format("Encrypting attachment %1$s to %2$s", att.getAttachedFile(), target)); try { FileTools.encrypt(att.getKey(), att.getAttachedFile(), target); att.setAttachedFile(null); } catch (final FileNotFoundException e) { disappearedFiles.add(att.getAttachedFile()); } } } if (!disappearedFiles.isEmpty()) { // we use just the first file for the error message and bail out final String filename = FileTools.getShortAbsoluteFilename(disappearedFiles.get(0)); throw new CryptoException(ERR_ATT_DISAPPEARED.format(new Object[] { filename })); } } /** * @param doc * the document */ private static void checkNewAttachments(TPMDocument doc) throws IOException, CryptoException { for (final Attachment att : doc.getAttachments()) { if (att.getAttachedFile() != null && !att.getAttachedFile().exists()) { final String filename = FileTools.getShortAbsoluteFilename(att.getAttachedFile()); throw new CryptoException(ERR_ATT_DISAPPEARED.format(new Object[] { filename })); } } } public static InputStream decrypt(final File file, InputStream is, final ICryptoContext ctx) throws PersistenceException, CryptoException { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(estimateLength(file)); ctx.decrypt(is, bos); is.close(); final byte[] decrypted = bos.toByteArray(); // final Field bufField = ByteArrayOutputStream.class.getDeclaredField("buf"); // bufField.setAccessible(true); // final byte[] bufValue = (byte[]) bufField.get(bos); // for (int i = 0; i < bufValue.length; i++) { // bufValue[i] = -1; // } bos = null; checkXmlHeader(file, decrypted); return new ByteArrayInputStream(decrypted); } catch (final IOException e) { throw new PersistenceException(e.getMessage(), e); } catch (final Exception e) { throw new ExceptionConverter(e); } } /** * removes the attachment from disk. * * @param att * the attachment to delete from disk */ private static void deleteAttachment(Attachment att) { final File file = createAttachmentFile(att); if (!file.delete()) { LOG.error("Could not delete the file" + file.getName()); } else { LOG.debug("File " + file.getName() + " deleted!"); } } private static void deleteOldBackups(final File file) { final File[] files = file.getParentFile().listFiles(new BackupFilter(file)); LOG.debug("found " + files.length + " backup files"); final List<File> sorted = new ArrayList<File>(Arrays.asList(files)); Collections.sort(sorted, FILE_BY_DATE); int toDelete = files.length - maxBackups + 1; for (int i = 0; i < sorted.size() && toDelete > 0; i++, toDelete--) { final File documentFile = sorted.get(i); LOG.debug("deleting document backup file " + documentFile.getName()); final String extension = getBackupExtension(documentFile); final String backupExtensionFilter = "(.*)" + Pattern.quote(extension); LOG.debug("backupExtensionFilter is " + backupExtensionFilter); for (final File f : listFiles(documentFile.getParentFile(), backupExtensionFilter)) { LOG.debug("Deleting backup file " + f); f.delete(); } } } /** * @param documentFile * the document backup file * @return the extension incl. BAK_ID */ static String getBackupExtension(File documentFile) { final Pattern pattern = Pattern.compile("(.*?)(" + Pattern.quote(BAK_ID) + "[0-9]{14})$"); final Matcher m = pattern.matcher(documentFile.getName()); if (!m.matches()) { throw new IllegalArgumentException(// String.format("The backup file %1$s has an invalid extension!", documentFile.getName())); } return m.group(2); } /** * @param doc * the document */ private static void deleteOrphanedAttachmentFiles(TPMDocument doc) { for (final File f : getOrphanedAttachmentFiles(doc)) { LOG.info("Deleting orphaned attachment file " + f); if (!f.delete()) { final String msg = String.format("The orphaned attachment file \"%1$s\" cannot be deleted!"); LOG.error(msg); } } } private static int estimateLength(final File file) { return (int) (10 * file.length()); } /** * @return the maximum number of backup files. */ public static final int getMaxBackups() { return maxBackups; } /** * @return all files without a representing attachment object. */ private static Collection<File> getOrphanedAttachmentFiles(TPMDocument doc) { final Set<File> knownFiles = new HashSet<File>(); for (final Attachment a : doc.getAttachments()) { final File attFile = createAttachmentFile(a); knownFiles.add(attFile.getAbsoluteFile()); } final Collection<File> listFiles = new HashSet<File>(); for (final File f : listAttachments(doc)) { listFiles.add(f.getAbsoluteFile()); } listFiles.removeAll(knownFiles); return listFiles; } /** * @return {@value #IS_DELETING_ORPHANS} */ private static boolean isDeletingOrphans() { return IS_DELETING_ORPHANS; } /** * @return all the attachments file objects in the directory, which are associated with this documents. Orphaned * files are listed as well. */ private static Collection<File> listAttachments(TPMDocument doc) { final File file = doc.getFile(); assert file.getParentFile().isDirectory(); return FileTools.listFiles(file.getParentFile(), createAttachmentNamePattern(file)); } /** * load the file. * * @param ctx * is the file to load. * @param handler * will report the warning and error warnings. * @throws KisKisException * if the password is not correct or the file could not be read. */ public static TPMDocument load(final ICryptoContext ctx, final IErrorHandler handler) throws PersistenceException, CryptoException { LOG.debug("loading " + ctx); InputStream is = null; final StopWatch w = new StopWatch(); final File file = ctx.getFile(); try { final FileFormats type = checkMimeType(file); is = new FileInputStream(file); switch (type) { case XML_FILE: break; case TRIPLEDES_FILE: case PGP_FILE: is = decrypt(file, is, ctx); break; default: throw new PersistenceException( ERR_UNKOWN_FILE.format(new Object[] { FileTools.getShortAbsoluteFilename(file) })); } final XMLReader xml = new XMLReader(ctx.getFile()); xml.setErrorHandler(handler); final TPMDocument doc = xml.load(is); checkAttachments(doc, handler); LOG.info("loaded in [length=" + file.length() + "]: " + w.getDuration()); return doc; } catch (final IOException e) { LOG.debug("load failed!", e); throw new PersistenceException(e.getMessage(), e); } finally { if (is != null) { try { is.close(); is = null; } catch (final IOException e) { LOG.error("close failed!", e); } } } } /** * saves the file to disc. * * @param pwd * the associated password or null if it should be stored as plain text. * @param createBackup * true, if a copy of the doc should be created. * @throws KisKisException * if anything is wrong. */ private static void save(final TPMDocument doc, final ICryptoContext ctx, final boolean createBackup) throws CryptoException, PersistenceException { LOG.debug("saving ctx=" + ctx + ", createBackup=" + createBackup); try { assert ctx.getFile().equals(doc.getFile()); final StopWatch w = new StopWatch(); checkNewAttachments(doc); LOG.info("new attachments checked in " + w.reset()); if (createBackup) { createBackup(doc); LOG.info("backup created in " + w.reset()); } final ByteArrayOutputStream bos = new ByteArrayOutputStream(); final XMLWriter writer = new XMLWriter(); writer.save(doc, bos); LOG.info("XML generated in " + w.reset()); final OutputStream os = new BufferedOutputStream(new FileOutputStream(doc.getFile())); ctx.encrypt(bos, os); os.flush(); os.close(); LOG.info("file written in " + w.reset()); createNewAttachments(doc); LOG.info("new attachments created in " + w.reset()); deleteOrphanedAttachmentFiles(doc); LOG.info("orphaned attachments cleaned in " + w.reset()); } catch (final IOException e) { throw new PersistenceException(e.getMessage(), e); } // set chmod on unix if possible final String chmod = "chmod 600 " + doc.getFile(); try { Runtime.getRuntime().exec(chmod); } catch (final IOException e) { LOG.info("cannot execute " + chmod + " ! msg=" + e.getMessage()); } } /** * save the file under a different name. * * @param ctx * is the crypto context to use for encryption. * @param createBackup * true, if a copy should be created. * @throws KisKisException * if anything is wrong */ public static void saveAs(final TPMDocument doc, final ICryptoContext ctx, final boolean createBackup) throws PersistenceException, CryptoException { final File file = ctx.getFile(); // we need to make sure that we can roll back this operation final File tmp = doc.getFile(); try { if (!doc.getFile().equals(file)) { copyAttachments(doc, file); } doc.setFile(file); save(doc, ctx, createBackup); } catch (final PersistenceException e) { doc.setFile(tmp); throw e; } catch (final CryptoException e) { doc.setFile(tmp); throw e; } } /** * @param target * the target file to store the decrypted data. * @throws KisKisException * if anything went wrong!: */ public static void saveAttachmentAs(Attachment att, final File target) throws PersistenceException { try { final File source = createAttachmentFile(att); if (!source.isFile()) { LOG.debug("Attachment " + source + " does not exist"); if (att.getAttachedFile() == null) { throw new PersistenceException( ERR_ATT_NOT_EXISTS.format(new Object[] { att.getDescription(), att.getId() })); } LOG.debug("Copying just attached file " + att.getAttachedFile()); FileProcessor.copy(att.getAttachedFile(), target); } else { LOG.debug("Decrypting file " + source + " to " + target); final char[] key = att.getKey(); FileTools.decrypt(key, source, target); } } catch (final Exception e) { throw new PersistenceException(e.getMessage(), e); } } /** * @param maxBackups * the maximum number of backup files. */ public static final void setMaxBackups(final int maxBackups) { PersistenceManager.maxBackups = maxBackups; P.putInt(K_MAX_BACKUPS, maxBackups); } /** * @return the result */ public static String checkCryptography() { final Collection<CryptoInstallationTester> all = new ArrayList<CryptoInstallationTester>(); final Map<String, CryptoInstallationTester> failed = new LinkedHashMap<String, CryptoInstallationTester>(); Map.Entry<String, SymmetricAlgo> firstOK = null; for (final Map.Entry<String, SymmetricAlgo> entry : SupportedAlgorithmsUtil.getSupportedAlgorithms() .entrySet()) { final SymmetricAlgo algo = entry.getValue(); final CryptoInstallationTester tester = new CryptoInstallationTester(entry.getKey(), algo); all.add(tester); tester.check(); if (tester.getException() != null) { tester.getException().printStackTrace(); failed.put(entry.getKey(), tester); } else if (firstOK == null) { firstOK = entry; } } final StringBuilder sb = new StringBuilder(M.getString("checkCryptography.PROLOG")); if (failed.isEmpty()) { sb.append(M.getString("checkCryptography.OK")); } else { final StringBuilder algos = new StringBuilder(); for (final Map.Entry<String, CryptoInstallationTester> t : failed.entrySet()) { final String msg = M.format("checkCryptography.FAILED_ALGO", // t.getKey()); algos.append(msg); } String defaultAlgo = "Not available"; if (firstOK != null) { defaultAlgo = firstOK.getKey(); Settings.setCryptoEngineClass(firstOK.getValue().getClass().getName()); } sb.append(M.format("checkCryptography.FAILED", algos.toString(), failed.size(), defaultAlgo)); } sb.append(M.getString("checkCryptography.OUTPUT_LABEL")); for (final CryptoInstallationTester tester : all) { sb.append(tester.getResult()); sb.append('\n'); } return sb.toString(); } }