/*
* Copyright (C) 2010-2016 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.helpers;
import com.jpexs.decompiler.flash.helpers.Freed;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author JPEXS
* @param <K>
* @param <V>
*/
public class FileHashMap<K, V> extends AbstractMap<K, V> implements Freed {
private static final Logger logger = Logger.getLogger(FileHashMap.class.getName());
private final Map<K, Integer> lengths = new HashMap<>();
private final Map<K, Long> offsets = new HashMap<>();
private long fileLen = 0;
private final RandomAccessFile file;
private final File fileName;
private final Set<Gap> gaps = new TreeSet<>();
private int maxGapLen = 0;
private boolean deleted = false;
private static class Gap implements Comparable<Gap> {
public long offset;
public int length;
public Gap(long offset, int length) {
this.offset = offset;
this.length = length;
}
@Override
public int compareTo(Gap o) {
return o.length - length;
}
@Override
public int hashCode() {
int hash = 7;
hash = 97 * hash + (int) (this.offset ^ (this.offset >>> 32));
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Gap other = (Gap) obj;
if (this.offset != other.offset) {
return false;
}
return true;
}
}
public static class FileEntry<K, V> implements Map.Entry<K, V> {
private final FileHashMap<K, V> parent;
private final K key;
public FileEntry(FileHashMap<K, V> parent, K key) {
this.parent = parent;
this.key = key;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return parent.get(key);
}
@Override
public V setValue(V value) {
return parent.put(key, value);
}
}
public FileHashMap(File file) throws IOException {
this.file = new RandomAccessFile(file, "rw");
this.file.setLength(0);
this.fileName = file;
file.deleteOnExit();
}
@Override
public boolean containsKey(Object key) {
if (deleted) {
throw new NullPointerException();
}
return offsets.containsKey(key);
}
@Override
public Set<K> keySet() {
if (deleted) {
throw new NullPointerException();
}
return offsets.keySet();
}
@Override
public V get(Object key) {
if (deleted) {
throw new NullPointerException();
}
try {
if (!offsets.containsKey(key)) {
return null;
}
long ofs = offsets.get(key);
int len = lengths.get(key);
file.seek(ofs);
byte[] data = new byte[len];
file.readFully(data);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
try {
@SuppressWarnings("unchecked")
V ret = (V) ois.readObject();
return ret;
} catch (ClassNotFoundException ex) {
logger.log(Level.SEVERE, null, ex);
return null;
}
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
return null;
}
}
@Override
public synchronized V put(K key, V value) {
if (deleted) {
throw new NullPointerException();
}
ObjectOutputStream oos = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(value);
oos.flush();
byte[] data = baos.toByteArray();
if (offsets.containsKey(key)) {
long origOffset = offsets.get(key);
int origLen = lengths.get(key);
if (data.length <= origLen) {
file.seek(origOffset);
file.write(data);
lengths.put(key, data.length);
if (data.length < origLen) {
Gap g = new Gap(origOffset + data.length, origLen - data.length);
if (g.length > maxGapLen) {
maxGapLen = g.length;
}
gaps.add(g);
}
return value;
}
}
if (data.length <= maxGapLen) {
for (Iterator<Gap> i = gaps.iterator(); i.hasNext();) {
Gap g = i.next();
if (g.length >= data.length) {
file.seek(g.offset);
file.write(data);
offsets.put(key, g.offset);
lengths.put(key, g.length);
if (g.length > data.length) {
g.offset = g.offset + data.length;
g.length = g.length - data.length;
} else {
i.remove();
}
}
}
} else {
file.seek(fileLen);
file.write(data);
offsets.put(key, fileLen);
lengths.put(key, data.length);
fileLen += data.length;
}
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
} finally {
try {
oos.close();
} catch (IOException ex) {
// ignore
}
}
return value;
}
@Override
public V remove(Object objKey) {
if (deleted) {
throw new NullPointerException();
}
@SuppressWarnings("unchecked")
K key = (K) objKey;
if (!containsKey(key)) {
return null;
}
V val = get((K) key);
Gap g = new Gap(offsets.get(key), lengths.get(key));
offsets.remove(key);
lengths.remove(key);
if (g.offset + g.length == fileLen) {
fileLen -= g.length;
} else {
if (g.length > maxGapLen) {
maxGapLen = g.length;
}
gaps.add(g);
}
return val;
}
@Override
public Set<Entry<K, V>> entrySet() {
if (deleted) {
throw new NullPointerException();
}
Set<Entry<K, V>> ret = new HashSet<>();
for (K key : keySet()) {
ret.add(new FileEntry<>(this, key));
}
return ret;
}
@Override
public void clear() {
if (deleted) {
throw new NullPointerException();
}
offsets.clear();
lengths.clear();
fileLen = 0;
maxGapLen = 0;
try {
file.setLength(0);
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
public void delete() {
if (deleted) {
throw new NullPointerException();
}
try {
file.close();
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
}
fileName.delete();
deleted = true;
}
@Override
public boolean isFreeing() {
return !deleted;
}
@Override
public void free() {
if (!deleted) {
delete();
}
}
@Override
public boolean isEmpty() {
return offsets.isEmpty();
}
@Override
public int size() {
return offsets.size();
}
}