/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.celements.photo.utilities;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.celements.photo.container.ImageLibStrings;
import com.celements.photo.image.GenerateThumbnail;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.api.Document;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.ListItem;
import com.xpn.xwiki.plugin.zipexplorer.ZipExplorerPluginAPI;
/*XXX
* WARNING: Do NOT change the List you get using
* <XWikiDocument>.getAttachmentList()! Changes in this List are not local, but
* change the original data in the XWikiDocument.
*/
public class ZipAttachmentChanges {
static Log mLogger = LogFactory.getFactory().getInstance(
ZipAttachmentChanges.class);
/**
* @param doc
* @param album
* @param context
* @throws XWikiException
* @throws IOException
*/
public void checkZipAttatchmentChanges(XWikiDocument doc, XWikiContext context) throws XWikiException, IOException {
//1. are there new / deleted zips? -> yes: read / delete
XWikiDocument albumDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_ALBUM, context);
/* This strange (and a bit ugly) construct of List and loops is
* necessary because actualAttachments is not independant from
* doc. Thus changes in this List will also change the attachments,
* attached to doc. Do NOT alter actualAttachments.
* ... and trust me, you won't like debugging errors grounded in such
* sideeffects ;-)
*/
List<BaseObject> notDeletedAttachments = new Vector<BaseObject>();
List<XWikiAttachment> notAddedAttachments = new Vector<XWikiAttachment>();
List<BaseObject> savedAttachmentList = (new BaseObjectHandler()).getAllFromBaseObjectList(albumDoc, ImageLibStrings.METATAG_ZIP_FILENAME);
List<XWikiAttachment> actualAttachments = doc.getAttachmentList();
if(savedAttachmentList != null){
for (Iterator<BaseObject> iter = savedAttachmentList.iterator(); iter.hasNext();) {
BaseObject attachmentObj = iter.next();
if(attachmentObj != null && actualAttachments != null){
for (Iterator<XWikiAttachment> iterator = actualAttachments.iterator(); iterator.hasNext();) {
XWikiAttachment attachment = iterator.next();
if((attachment != null) && attachment.getFilename().equals(attachmentObj.getStringValue(ImageLibStrings.METAINFO_CLASS_DESCRIPTION))){
notAddedAttachments.add(attachment);
notDeletedAttachments.add(attachmentObj);
break;
}
}
}
}
removeDataFromRemovedZip(savedAttachmentList, notDeletedAttachments, doc, albumDoc, context);
}
addRemainingZips(actualAttachments, notAddedAttachments, doc, albumDoc, context);
}
private void removeDataFromRemovedZip(List<BaseObject> savedAttachmentList,
List<BaseObject> notDeletedAttachments, XWikiDocument doc,
XWikiDocument albumDoc, XWikiContext context) throws XWikiException {
if(savedAttachmentList.size() > notDeletedAttachments.size()){
for (Iterator<BaseObject> iter = savedAttachmentList.iterator(); iter.hasNext();) {
BaseObject element = iter.next();
boolean delete = true;
for (Iterator<BaseObject> iterator = notDeletedAttachments.iterator(); iterator.hasNext();) {
BaseObject ele = iterator.next();
if(element.equals(ele)){
delete = false;
break;
}
}
deleteImagesFromZip(element, delete, doc, albumDoc, context);
}
}
}
private void deleteImagesFromZip(BaseObject element, boolean delete,
XWikiDocument doc, XWikiDocument albumDoc, XWikiContext context)
throws XWikiException {
if(delete){
String filename = "";
List<String> fileList = context.getWiki().getSpaceDocsName(ImageLibStrings.getPhotoSpace(doc), context);
if(fileList != null){
Iterator<String> iterator = fileList.iterator();
while(iterator.hasNext() && !filename.equals(doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_ZIP + getFilenameHash(element.getStringValue(ImageLibStrings.METAINFO_CLASS_DESCRIPTION)))){
filename = iterator.next();
}
}
deleteThumbAndMetaForAll(element, doc, filename, albumDoc, context);
}
}
private void deleteThumbAndMetaForAll(BaseObject element, XWikiDocument doc,
String filename, XWikiDocument albumDoc, XWikiContext context)
throws XWikiException {
XWikiDocument deletedZip = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), filename, context);
List<BaseObject> imagesInZip = (new BaseObjectHandler()).getAllFromBaseObjectList(deletedZip, ImageLibStrings.METATAG_IMAGE_HASH);
for (Iterator<BaseObject> iterator = imagesInZip.iterator(); iterator.hasNext();) {
BaseObject imageHashObj = iterator.next();
String imageHash = imageHashObj.getStringValue(ImageLibStrings.METAINFO_CLASS_DESCRIPTION);
deleteThumbnail(doc, imageHash, context);
//ii. loesche meta
deleteMetainfo(doc, imageHash, context);
//iii. loesche celements meta oder - falls mehrere versionen vorhanden: update
updateOrDeleteMetaData(imageHash, doc, context);
}
context.getWiki().deleteDocument(deletedZip, context);
albumDoc.removeObject(element);
context.getWiki().saveDocument(albumDoc, context);
}
private void updateOrDeleteMetaData(String imageHash, XWikiDocument doc,
XWikiContext context) throws XWikiException {
XWikiDocument celementsMetaDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_IMAGE + imageHash, context);
int revision = (new BaseObjectHandler()).getImageInteger(celementsMetaDoc, ImageLibStrings.PHOTO_IMAGE_REVISION);
if(revision <= 1){
context.getWiki().deleteDocument(celementsMetaDoc, context);
} else{
BaseObjectHandler handler = new BaseObjectHandler();
handler.setImageInteger(celementsMetaDoc, ImageLibStrings.PHOTO_IMAGE_REVISION, revision-1, context);
List<XWikiAttachment> attachmentList = doc.getAttachmentList();
Collections.sort(attachmentList, new FileDateComparator());
String filenameInMeta = null;
XWikiAttachment att = null;
for (Iterator<XWikiAttachment> attIter = attachmentList.iterator(); attIter.hasNext();) {
att = attIter.next();
XWikiDocument zipAttDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_ZIP + getFilenameHash(att.getFilename()), context);
filenameInMeta = handler.getDescriptionFromBaseObjectList(zipAttDoc, imageHash);
if(filenameInMeta != null){
break;
}
}
setNameAndDirectory(filenameInMeta, att, handler, celementsMetaDoc, context);
}
}
private void setNameAndDirectory(String filenameInMeta, XWikiAttachment att,
BaseObjectHandler handler, XWikiDocument celementsMetaDoc,
XWikiContext context) throws XWikiException {
// ==null might occur when there is no existing version anymore and has not been removed correctly
if(filenameInMeta != null){
handler.setImageString(celementsMetaDoc, ImageLibStrings.PHOTO_IMAGE_ZIPNAME, att.getFilename(), context);
String [] path = filenameInMeta.split(System.getProperty("file.separator"));
String dir = "";
for(int i = 0; i < (path.length-1); i++){
dir = dir + path[i] + "/";
}
handler.setImageString(celementsMetaDoc, ImageLibStrings.PHOTO_IMAGE_ZIPDIRECTORY, dir, context);
handler.setImageString(celementsMetaDoc, ImageLibStrings.PHOTO_IMAGE_FILENAME, path[path.length-1], context);
} else{
context.getWiki().deleteDocument(celementsMetaDoc, context);
}
}
private void addRemainingZips(List<XWikiAttachment> actualAttachments,
List<XWikiAttachment> notAddedAttachments, XWikiDocument doc,
XWikiDocument albumDoc, XWikiContext context) throws XWikiException,
IOException {
if(actualAttachments.size() > notAddedAttachments.size()){
for (Iterator<XWikiAttachment> iter = actualAttachments.iterator(); iter.hasNext();) {
XWikiAttachment element = iter.next();
boolean added = true;
for (Iterator<XWikiAttachment> iterator = notAddedAttachments.iterator(); iterator.hasNext();) {
XWikiAttachment ele = iterator.next();
if(element.equals(ele)){
added = false;
}
}
getImagesFromZip(element, added, doc, albumDoc, context);
}
}
}
private void getImagesFromZip(XWikiAttachment element, boolean added,
XWikiDocument doc, XWikiDocument albumDoc, XWikiContext context)
throws XWikiException, IOException {
String mimeType = element.getMimeType(context);
if(added && ((mimeType.equalsIgnoreCase(ImageLibStrings.MIME_ZIP) || mimeType.equalsIgnoreCase(ImageLibStrings.MIME_ZIP_MICROSOFT)))){
BaseObjectHandler handler = new BaseObjectHandler();
List<String> hashes = generateListOfHashes(element, handler, doc, context);
XWikiDocument zipMetaDoc = writeHashesToMetaDoc(element, doc, context);
for (Iterator<String> iterator = hashes.iterator(); iterator.hasNext();) {
String hash = iterator.next();
XWikiDocument imgDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_IMAGE + hash, context);
String dir = handler.getImageString(imgDoc, ImageLibStrings.PHOTO_IMAGE_ZIPDIRECTORY);
String filename = handler.getImageString(imgDoc, ImageLibStrings.PHOTO_IMAGE_FILENAME);
handler.addBaseObject(zipMetaDoc, ImageLibStrings.METATAG_IMAGE_HASH, hash, context);
handler.addBaseObject(zipMetaDoc, hash, dir + filename, context);
}
handler.addBaseObject(albumDoc, ImageLibStrings.METATAG_ZIP_FILENAME, element.getFilename(), context);
}
}
private List<String> generateListOfHashes(XWikiAttachment element, BaseObjectHandler handler, XWikiDocument doc,
XWikiContext context) throws XWikiException, IOException {
List<String> hashes = new Vector<String>();
List<ListItem> fileList = getZipExplorerPluginApi(context).getFileTreeList(new Document(doc, context), element.getFilename());
for (Iterator<ListItem> fileIter = fileList.iterator(); fileIter.hasNext();) {
ListItem file = (ListItem) fileIter.next();
String dir = file.getParent();
String filename = file.getValue();
String extention = filename.substring(filename.lastIndexOf(".")+1);
if(file.getId().endsWith(System.getProperty("file.separator")) ||
dir.contains("__MACOSX") ||
!((extention.equalsIgnoreCase(ImageLibStrings.MIME_JPG) ||
extention.equalsIgnoreCase(ImageLibStrings.MIME_JPEG)))){
continue;
}
InputStream image = getFromZip(element, dir + filename, context);
String hash = (new GenerateThumbnail()).hashImage(image);
hashes.add(hash);
XWikiDocument imgMetaDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_IMAGE + hash, context);
if(imgMetaDoc.isNew()){
imgMetaDoc.newObject(ImageLibStrings.PHOTO_IMAGE_CLASS, context);
handler.setImageString(imgMetaDoc, ImageLibStrings.PHOTO_IMAGE_HASH, hash, context);
}else if(imgMetaDoc.getObject(ImageLibStrings.PHOTO_IMAGE_CLASS) != null){
deleteThumbnail(doc, hash, context);
deleteMetainfo(doc, hash, context);
}
updateMetaObject(element, dir, filename, handler, imgMetaDoc, context);
}
return hashes;
}
private XWikiDocument writeHashesToMetaDoc(XWikiAttachment element,
XWikiDocument doc, XWikiContext context) throws XWikiException {
XWikiDocument zipMetaDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_ZIP + getFilenameHash(element.getFilename()), context);
if(!zipMetaDoc.isNew()){
context.getWiki().deleteDocument(zipMetaDoc, context);
zipMetaDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_ZIP + getFilenameHash(element.getFilename()), context);
}
return zipMetaDoc;
}
private int updateMetaObject(XWikiAttachment element, String dir,
String filename, BaseObjectHandler handler, XWikiDocument imgMetaDoc,
XWikiContext context) throws XWikiException {
int revision = handler.getImageInteger(imgMetaDoc, ImageLibStrings.PHOTO_IMAGE_REVISION);
handler.setImageInteger(imgMetaDoc, ImageLibStrings.PHOTO_IMAGE_REVISION, revision+1, context);
handler.setImageString(imgMetaDoc, ImageLibStrings.PHOTO_IMAGE_ZIPNAME, element.getFilename(), context);
handler.setImageString(imgMetaDoc, ImageLibStrings.PHOTO_IMAGE_ZIPDIRECTORY, dir, context);
handler.setImageString(imgMetaDoc, ImageLibStrings.PHOTO_IMAGE_FILENAME, filename, context);
return revision;
}
/**
* Extract the specified image from the zip file and return it as an
* InputStream.
*
* @param album XWikiDocument of the album the image is part of.
* @param image Name of the image.
* @param id Id of the image.
* @param context XWikiContext
* @return ImputStream representation of the image.
* @throws XWikiException
* @throws IOException
*/
public InputStream getFromZip(XWikiDocument album, String image, String id, XWikiContext context) throws XWikiException, IOException {
XWikiAttachment attachment = getContainingZip(album, id, context);
return getFromZip(attachment, image, context);
}
/**
* Get the zip attachment containing the specified image.
*
* @param doc XWikiDocument of the album.
* @param id Id of the image.
* @param context XWikiContext
* @return Returns an XWikiAttachment representation of the zip file,
* containing the specified image.
* @throws XWikiException
*/
@SuppressWarnings("unchecked")
public XWikiAttachment getContainingZip(XWikiDocument doc, String id, XWikiContext context) throws XWikiException {
XWikiDocument imageMeta = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_IMAGE + id, context);
String zipName = (new BaseObjectHandler()).getImageString(imageMeta, ImageLibStrings.PHOTO_IMAGE_ZIPNAME);
List<XWikiAttachment> attachments = doc.getAttachmentList();
for (Iterator iter = attachments.iterator(); iter.hasNext();) {
XWikiAttachment attachment = (XWikiAttachment) iter.next();
if(attachment.getFilename().equals(zipName)){
return attachment;
}
}
return null;
}
/**
* Extract the specified image from the zip file and return it as an
* XWikiAttachment.
*
* @param zip XWikiAttachment representation of the zip archive the image
* is archived in.
* @param image Name of the image.
* @param context XWikiContext
* @return InputStream representation of the image.
* @throws XWikiException
* @throws IOException
*/
public InputStream getFromZip(XWikiAttachment zip, String image, XWikiContext context) throws XWikiException, IOException {
if(image != null){
byte[] imageArray;
imageArray = (new Unzip()).getFile(zip.getContent(context), image).toByteArray();
return new ByteArrayInputStream(imageArray);
}
return null;
}
/**
* Deletes the metainformation of the specified image (not the celements
* specific image information).
*
* @param doc XWikiDocument of the album.
* @param imageHash Hash of the image
* @param context XWikiContext
* @throws XWikiException
*/
private void deleteMetainfo(XWikiDocument doc, String imageHash, XWikiContext context) throws XWikiException {
XWikiDocument metaDocument = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_IMAGE + imageHash, context);
if(!metaDocument.isNew()){
metaDocument.removeObjects(ImageLibStrings.METAINFO_CLASS);
context.getWiki().saveDocument(metaDocument, context);
}
}
/**
* Deletes all thumbnails of the specified image.
*
* @param doc XWikiDocument of the album.
* @param imageHash Hash of the image
* @param context XWikiContext
* @throws XWikiException
*/
private void deleteThumbnail(XWikiDocument doc, String imageHash, XWikiContext context) throws XWikiException {
XWikiDocument thumbDoc = context.getWiki().getDocument(ImageLibStrings.getPhotoSpace(doc), doc.getName() + ImageLibStrings.DOCUMENT_SEPARATOR_IMAGE + imageHash, context);
List<XWikiAttachment> thumbnails = thumbDoc.getAttachmentList();
for (Iterator<XWikiAttachment> deleteIter = thumbnails.iterator(); deleteIter.hasNext();) {
XWikiAttachment delete = (XWikiAttachment) deleteIter.next();
thumbDoc.deleteAttachment(delete, context);
}
}
private String getFilenameHash(String filename){
try {
MessageDigest digest = MessageDigest.getInstance(ImageLibStrings.HASHING_ALGORITHM);
digest.update(filename.getBytes());
String hashraw = new String(digest.digest());
return Util.getUtil().hashToHex(hashraw);
} catch (NoSuchAlgorithmException e) {
mLogger.error(e);
}
return filename.replaceAll("_", "__");
}
/**
* Returns the ZipExplorerPluginApi from the active XWiki.
*
* @param context The XWikiContext.
* @return The ZipExplorerPluginApi.
*/
public ZipExplorerPluginAPI getZipExplorerPluginApi(XWikiContext context) {
return (ZipExplorerPluginAPI)context.getWiki().getPluginApi("zipexplorer", context);
}
}