/**
* Copyright 2011-2012 Alexandre Dutra
*
* 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 fr.dutra.confluence2wordpress.core.sync;
import static com.atlassian.confluence.content.render.xhtml.XhtmlConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.apache.commons.io.IOUtils;
import com.atlassian.confluence.core.ContentEntityObject;
import com.atlassian.confluence.pages.Attachment;
import com.atlassian.confluence.pages.AttachmentManager;
import com.atlassian.confluence.pages.PageManager;
import fr.dutra.confluence2wordpress.core.metadata.Metadata;
import fr.dutra.confluence2wordpress.core.settings.PluginSettingsManager;
import fr.dutra.confluence2wordpress.util.StaxUtils;
import fr.dutra.confluence2wordpress.wp.WordpressClient;
import fr.dutra.confluence2wordpress.wp.WordpressFile;
import fr.dutra.confluence2wordpress.wp.WordpressXmlRpcException;
public class DefaultAttachmentsSynchronizer implements AttachmentsSynchronizer {
/*
* <ac:image ac:title="foo.jpg">
* <ri:attachment ri:filename="foo.jpg">
* <ri:page ri:content-title="title" ri:space-key="sk" />
* </ri:attachment>
* </ac:image>
*/
private static final QName ATTACHMENT_QNAME = new QName(RESOURCE_IDENTIFIER_NAMESPACE_URI, "attachment");
private static final QName FILENAME_QNAME = new QName(RESOURCE_IDENTIFIER_NAMESPACE_URI, "filename");
private static final QName PAGE_QNAME = new QName(RESOURCE_IDENTIFIER_NAMESPACE_URI, "page");
private static final QName TITLE_QNAME = new QName(RESOURCE_IDENTIFIER_NAMESPACE_URI, "content-title");
private static final QName SPACE_QNAME = new QName(RESOURCE_IDENTIFIER_NAMESPACE_URI, "space-key");
private AttachmentManager attachmentManager;
private PageManager pageManager;
private PluginSettingsManager pluginSettingsManager;
public DefaultAttachmentsSynchronizer(AttachmentManager attachmentManager, PageManager pageManager, PluginSettingsManager pluginSettingsManager) {
super();
this.attachmentManager = attachmentManager;
this.pageManager = pageManager;
this.pluginSettingsManager = pluginSettingsManager;
}
public List<SynchronizedAttachment> synchronizeAttachments(ContentEntityObject page, Metadata metadata) throws SynchronizationException, WordpressXmlRpcException {
Set<Attachment> attachments = parseForAttachments(page);
removeOldAttachments(metadata, attachments);
if(attachments == null || attachments.isEmpty()){
return null;
}
Set<Attachment> toUpload = findNewAttachments(metadata, attachments);
List<SynchronizedAttachment> uploaded = uploadAttachments(toUpload);
ArrayList<SynchronizedAttachment> newAttachments = mergeOldAndNewAttachments(metadata, uploaded);
metadata.setAttachments(newAttachments);
return newAttachments;
}
private Set<Attachment> parseForAttachments(ContentEntityObject page) throws SynchronizationException {
Set<Attachment> attachments = new HashSet<Attachment>();
try {
XMLEventReader r = StaxUtils.getReader(page);
String fileName = null;
String pageTitle = null;
String spaceKey = null;
try {
while(r.hasNext()){
XMLEvent e = r.nextEvent();
if(e.isStartElement()){
StartElement startElement = e.asStartElement();
QName name = startElement.getName();
if(name.equals(ATTACHMENT_QNAME)) {
Attribute att = startElement.getAttributeByName(FILENAME_QNAME);
if(att != null){
fileName = att.getValue();
}
} else if(name.equals(PAGE_QNAME)) {
Attribute title = startElement.getAttributeByName(TITLE_QNAME);
if(title != null){
pageTitle = title.getValue();
}
Attribute space = startElement.getAttributeByName(SPACE_QNAME);
if(space != null){
spaceKey = space.getValue();
}
}
} else if (e.isEndElement()) {
EndElement endElement = e.asEndElement();
if(endElement.getName().equals(ATTACHMENT_QNAME)) {
ContentEntityObject attachmentPage;
if(pageTitle == null) {
attachmentPage = page;
} else {
attachmentPage = pageManager.getPage(spaceKey, pageTitle);
}
Attachment attachment = attachmentManager.getAttachment(attachmentPage, fileName);
attachments.add(attachment);
fileName = null;
pageTitle = null;
spaceKey = null;
}
}
}
} finally {
r.close();
}
} catch (XMLStreamException e) {
throw new SynchronizationException("Cannot read page: " + page.getTitle(), e);
}
return attachments;
}
private void removeOldAttachments(Metadata metadata, Set<Attachment> attachments) {
if(attachments == null || attachments.isEmpty()) {
metadata.setAttachments(null);
return;
}
List<SynchronizedAttachment> metadataAttachments = metadata.getAttachments();
if(metadataAttachments == null || metadataAttachments.isEmpty()) {
return;
}
Iterator<SynchronizedAttachment> it = metadataAttachments.iterator();
outer: while(it.hasNext()){
SynchronizedAttachment sa = it.next();
for (Attachment attachment : attachments) {
if(attachment.getId() == sa.getAttachmentId()) {
continue outer;
}
}
it.remove();
}
}
private Set<Attachment> findNewAttachments(Metadata metadata, Set<Attachment> attachments) {
Set<Attachment> newAttachments = new HashSet<Attachment>();
List<SynchronizedAttachment> metadataAttachments = metadata.getAttachments();
if(metadataAttachments == null || metadataAttachments.isEmpty()) {
newAttachments.addAll(attachments);
} else {
outer: for (Attachment attachment : attachments) {
for (SynchronizedAttachment metadataAttachment : metadataAttachments) {
if(metadataAttachment.getAttachmentId() == attachment.getId()) {
Integer version = metadataAttachment.getAttachmentVersion();
if (version == null || attachment.getAttachmentVersion() > version){
newAttachments.add(attachment);
}
continue outer;
}
}
newAttachments.add(attachment);
}
}
return newAttachments;
}
private ArrayList<SynchronizedAttachment> mergeOldAndNewAttachments(Metadata metadata, List<SynchronizedAttachment> uploaded) {
Set<SynchronizedAttachment> metadataAttachments = new HashSet<SynchronizedAttachment>();
//modified first
if(uploaded != null) {
metadataAttachments.addAll(uploaded);
}
if(metadata.getAttachments() != null) {
metadataAttachments.addAll(metadata.getAttachments());
}
ArrayList<SynchronizedAttachment> newAttachments = new ArrayList<SynchronizedAttachment>(metadataAttachments);
return newAttachments;
}
private List<SynchronizedAttachment> uploadAttachments(Set<Attachment> attachments) throws WordpressXmlRpcException, SynchronizationException {
if(attachments == null || attachments.isEmpty()) {
return null;
}
int size = attachments.size();
final WordpressClient client = pluginSettingsManager.getWordpressClient();
List<FutureHolder> futures = new ArrayList<FutureHolder>(size);
for (final Attachment attachment : attachments) {
byte[] data;
try {
data = IOUtils.toByteArray(attachment.getContentsAsStream());
} catch (IOException e) {
throw new SynchronizationException("Cannot read attachment: " + attachment.getFileName(), e);
}
WordpressFile file = new WordpressFile(
attachment.getFileName(),
attachment.getContentType(),
data);
futures.add(new FutureHolder(attachment, client.uploadFile(file)));
}
List<SynchronizedAttachment> synchronizedAttachments = new ArrayList<SynchronizedAttachment>(size);
for (FutureHolder future : futures) {
try {
SynchronizedAttachment synchronizedAttachment = future.toSynchronizedAttachment();
synchronizedAttachments.add(synchronizedAttachment);
} catch (InterruptedException e) {
throw new WordpressXmlRpcException("Cannot upload attachment", e);
} catch (ExecutionException e) {
if(e.getCause() instanceof WordpressXmlRpcException) {
throw (WordpressXmlRpcException) e.getCause();
}
throw new WordpressXmlRpcException("Cannot upload attachment", e.getCause() == null ? e : e.getCause());
}
}
return synchronizedAttachments;
}
private class FutureHolder {
private Attachment attachment;
private Future<WordpressFile> future;
private FutureHolder(Attachment attachment, Future<WordpressFile> future) {
super();
this.attachment = attachment;
this.future = future;
}
private SynchronizedAttachment toSynchronizedAttachment() throws InterruptedException, ExecutionException{
return new SynchronizedAttachment(attachment, future.get());
}
}
}