package fitnesse.wiki.fs;
import fitnesse.wiki.NoSuchVersionException;
import fitnesse.wiki.VersionInfo;
import fitnesse.wiki.WikiImportProperty;
import util.FileUtil;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.*;
import static fitnesse.ConfigurationParameter.VERSIONS_CONTROLLER_DAYS;
import static java.lang.String.format;
public class ZipFileVersionsController implements VersionsController {
private static final Logger LOG = Logger.getLogger(ZipFileVersionsController.class.getName());
private static final Pattern ZIP_FILE_PATTERN = Pattern.compile("(\\S+)?\\d+(~\\d+)?\\.zip");
public static final String ZIP_EXTENSION = ".zip";
private final int daysTillVersionsExpire;
private VersionsController persistence;
public ZipFileVersionsController(Properties properties) {
this(getVersionDays(properties));
}
public ZipFileVersionsController() {
this(14);
}
public ZipFileVersionsController(int versionDays) {
this.daysTillVersionsExpire = versionDays;
// Fix on Disk file system, since that's what ZipFileVersionsController can deal with.
persistence = new SimpleFileVersionsController(new DiskFileSystem());
}
private static int getVersionDays(Properties properties) {
String days = properties.getProperty(VERSIONS_CONTROLLER_DAYS.getKey());
return days == null ? 14 : Integer.parseInt(days);
}
@Override
public FileVersion[] getRevisionData(final String label, final File... files) throws IOException {
if (label == null) {
return persistence.getRevisionData(null, files);
}
final File file = new File(files[0].getParentFile(), label + ZIP_EXTENSION);
if (!file.exists()) {
throw new NoSuchVersionException("There is no version '" + label + "'");
}
ZipFile zipFile = null;
FileVersion[] versions = new FileVersion[files.length];
int counter = 0;
try {
zipFile = new ZipFile(file);
for (File f : files) {
ZipFileVersion version = loadZipEntry(zipFile, f);
if (version != null)
versions[counter++] = version;
}
return versions;
} finally {
FileUtil.close(zipFile);
}
}
@Override
public Collection<VersionInfo> history(final File... pageFiles) {
// Let's assume for a second all files live in the same folder
File dir = commonBaseDir(pageFiles);
final File[] files = dir.listFiles();
final Set<VersionInfo> versions = new HashSet<>();
if (files != null) {
for (final File file : files) {
if (isVersionFile(file) && isZipForFiles(file, pageFiles)) {
versions.add(ZipFileVersionInfo.makeVersionInfo(file));
}
}
}
return versions;
}
// What about a page with just a content.txt file and no properties.xml?
// Is okay to have one of the files present!
private boolean isZipForFiles(File zipFile, File... containedFiles) {
List<String> zipFileNames = getFileNamesInZipFile(zipFile);
for (File f : containedFiles) {
if (zipFileNames.contains(f.getName())) {
return true;
}
}
return false;
}
private List<String> getFileNamesInZipFile(File zipFile) {
List<String> names = new ArrayList<>();
try (final ZipInputStream zos = new ZipInputStream(new FileInputStream(zipFile))) {
for (ZipEntry entry = zos.getNextEntry(); entry != null; entry = zos.getNextEntry()) {
names.add(entry.getName());
}
} catch (IOException e) {
LOG.log(Level.WARNING, format("Could not read zip file %s", zipFile), e);
}
return names;
}
@Override
public VersionInfo makeVersion(final FileVersion... fileVersions) throws IOException {
File commonBaseDir = commonBaseDir(fileVersions);
String versionName = makeVersionName(commonBaseDir, fileVersions[0]);
final File zipFile = new File(commonBaseDir, versionName + ZIP_EXTENSION);
makeZipVersion(zipFile, fileVersions);
pruneVersions(history(toFiles(fileVersions)));
persistence.makeVersion(fileVersions);
return new VersionInfo(versionName, fileVersions[0].getAuthor(), fileVersions[0].getLastModificationTime());
}
@Override
public VersionInfo addDirectory(FileVersion filePath) throws IOException {
return persistence.addDirectory(filePath);
}
@Override
public void rename(FileVersion fileVersion, File originalFile) throws IOException {
persistence.rename(fileVersion, originalFile);
}
@Override
public void delete(File... files) throws IOException {
persistence.delete(files);
}
protected void makeZipVersion(File zipFile, FileVersion... fileVersions) throws IOException {
if (!exists(fileVersions)) {
return;
}
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(new FileOutputStream(zipFile));
for (FileVersion fileVersion : fileVersions) {
addToZip(fileVersion.getFile(), zos);
}
} finally {
try {
if (zos != null) {
zos.finish();
FileUtil.close(zos);
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Unable to create zip file", e);
}
}
}
private boolean exists(FileVersion[] fileVersions) {
for (FileVersion fileVersion : fileVersions) {
if (fileVersion.getFile().exists())
return true;
}
return false;
}
private File[] toFiles(FileVersion[] fileVersions) {
File[] files = new File[fileVersions.length];
for (int i = 0; i < fileVersions.length; i++) {
files[i] = fileVersions[i].getFile();
}
return files;
}
private File commonBaseDir(FileVersion[] fileVersions) {
return commonBaseDir(fileVersions[0].getFile());
}
private File commonBaseDir(File... files) {
return files[0].getParentFile();
}
private void addToZip(final File file, final ZipOutputStream zos) throws IOException {
final FileInputStream is;
try {
is = new FileInputStream(file);
} catch (FileNotFoundException e) {
LOG.warning("File " + file.getAbsolutePath() + " not found. It can not be saved in this version.");
return;
}
try {
final ZipEntry entry = new ZipEntry(file.getName());
zos.putNextEntry(entry);
final int size = (int) file.length();
final byte[] bytes = new byte[size];
is.read(bytes);
zos.write(bytes, 0, size);
} finally {
FileUtil.close(is);
}
}
private boolean isVersionFile(final File file) {
return ZIP_FILE_PATTERN.matcher(file.getName()).matches();
}
private ZipFileVersion loadZipEntry(final ZipFile zipFile, final File file) throws IOException {
final ZipEntry contentEntry = zipFile.getEntry(file.getName());
if (contentEntry != null) {
InputStream contentIS = null;
try {
contentIS = zipFile.getInputStream(contentEntry);
return new ZipFileVersion(file, FileUtil.toString(contentIS), new Date(contentEntry.getTime()));
} finally {
try {
if (contentIS != null) {
contentIS.close();
}
} catch (Exception e) {
LOG.log(Level.WARNING, "Unable to read zip file contents", e);
}
}
}
return null;
}
private String makeVersionName(final File baseDir, final FileVersion fileVersion) {
Date time = fileVersion.getLastModificationTime();
String versionName = WikiImportProperty.getTimeFormat().format(time);
final String user = fileVersion.getAuthor();
if (user != null && !"".equals(user)) {
versionName = user + "-" + versionName;
}
String name = versionName;
int counter = 1;
while (new File(baseDir, name + ZIP_EXTENSION).exists()) {
name = versionName + "~" + (counter++);
}
return name;
}
private void pruneVersions(Collection<VersionInfo> versions) {
List<VersionInfo> versionsList = makeSortedVersionList(versions);
if (!versions.isEmpty()) {
VersionInfo lastVersion = versionsList.get(versionsList.size() - 1);
Date expirationDate = makeVersionExpirationDate(lastVersion);
for (VersionInfo version : versionsList) {
Date thisDate = version.getCreationTime();
if (thisDate.before(expirationDate) || thisDate.equals(expirationDate))
((ZipFileVersionInfo) version).getFile().delete();
}
}
}
private List<VersionInfo> makeSortedVersionList(Collection<VersionInfo> versions) {
List<VersionInfo> versionsList = new ArrayList<>(versions);
Collections.sort(versionsList);
return versionsList;
}
private Date makeVersionExpirationDate(VersionInfo lastVersion) {
Date dateOfLastVersion = lastVersion.getCreationTime();
GregorianCalendar expirationDate = new GregorianCalendar();
expirationDate.setTime(dateOfLastVersion);
expirationDate.add(Calendar.DAY_OF_MONTH, -(daysTillVersionsExpire));
return expirationDate.getTime();
}
@Override
public String toString() {
return this.getClass().getSimpleName();
}
private static class ZipFileVersion implements FileVersion {
private final File file;
private final String content;
private final Date lastModified;
public ZipFileVersion(File file, String content, Date lastModified) {
this.file = file;
this.content = content;
this.lastModified = lastModified;
}
@Override
public File getFile() {
return file;
}
@Override
public InputStream getContent() throws IOException {
return new ByteArrayInputStream(content.getBytes(FileUtil.CHARENCODING));
}
@Override
public String getAuthor() {
return null;
}
@Override
public Date getLastModificationTime() {
return lastModified;
}
}
}