b7d448acc6b3cc6708ded57c564cac66b9e1f66c
[jSite.git] / src / de / todesbaum / jsite / application / ProjectInserter.java
1 /*
2  * jSite - a tool for uploading websites into Freenet
3  * Copyright (C) 2006 David Roden
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 package de.todesbaum.jsite.application;
21
22 import java.io.ByteArrayInputStream;
23 import java.io.ByteArrayOutputStream;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.util.ArrayList;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35 import java.util.zip.ZipEntry;
36 import java.util.zip.ZipOutputStream;
37
38 import de.todesbaum.jsite.gui.FileScanner;
39 import de.todesbaum.jsite.gui.FileScannerListener;
40 import de.todesbaum.util.freenet.fcp2.Client;
41 import de.todesbaum.util.freenet.fcp2.ClientPutComplexDir;
42 import de.todesbaum.util.freenet.fcp2.Connection;
43 import de.todesbaum.util.freenet.fcp2.DirectFileEntry;
44 import de.todesbaum.util.freenet.fcp2.FileEntry;
45 import de.todesbaum.util.freenet.fcp2.Message;
46 import de.todesbaum.util.freenet.fcp2.RedirectFileEntry;
47 import de.todesbaum.util.freenet.fcp2.Verbosity;
48 import de.todesbaum.util.io.Closer;
49 import de.todesbaum.util.io.ReplacingOutputStream;
50 import de.todesbaum.util.io.StreamCopier;
51
52 /**
53  * Manages project inserts.
54  *
55  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
56  */
57 public class ProjectInserter implements FileScannerListener, Runnable {
58
59         /** The logger. */
60         private static final Logger logger = Logger.getLogger(ProjectInserter.class.getName());
61
62         /** Random number for FCP instances. */
63         private static final int random = (int) (Math.random() * Integer.MAX_VALUE);
64
65         /** Counter for FCP connection identifier. */
66         private static int counter = 0;
67
68         /** The list of insert listeners. */
69         private List<InsertListener> insertListeners = new ArrayList<InsertListener>();
70
71         /** The freenet interface. */
72         protected Freenet7Interface freenetInterface;
73
74         /** The project to insert. */
75         protected Project project;
76
77         /** The file scanner. */
78         private FileScanner fileScanner;
79
80         /** Object used for synchronization. */
81         protected final Object lockObject = new Object();
82
83         /**
84          * Adds a listener to the list of registered listeners.
85          *
86          * @param insertListener
87          *            The listener to add
88          */
89         public void addInsertListener(InsertListener insertListener) {
90                 insertListeners.add(insertListener);
91         }
92
93         /**
94          * Removes a listener from the list of registered listeners.
95          *
96          * @param insertListener
97          *            The listener to remove
98          */
99         public void removeInsertListener(InsertListener insertListener) {
100                 insertListeners.remove(insertListener);
101         }
102
103         /**
104          * Notifies all listeners that the project insert has started.
105          *
106          * @see InsertListener#projectInsertStarted(Project)
107          */
108         protected void fireProjectInsertStarted() {
109                 for (InsertListener insertListener : insertListeners) {
110                         insertListener.projectInsertStarted(project);
111                 }
112         }
113
114         /**
115          * Notifies all listeners that the insert has generated a URI.
116          *
117          * @see InsertListener#projectURIGenerated(Project, String)
118          * @param uri
119          *            The generated URI
120          */
121         protected void fireProjectURIGenerated(String uri) {
122                 for (InsertListener insertListener : insertListeners) {
123                         insertListener.projectURIGenerated(project, uri);
124                 }
125         }
126
127         /**
128          * Notifies all listeners that the insert has made some progress.
129          *
130          * @see InsertListener#projectUploadFinished(Project)
131          */
132         protected void fireProjectUploadFinished() {
133                 for (InsertListener insertListener : insertListeners) {
134                         insertListener.projectUploadFinished(project);
135                 }
136         }
137
138         /**
139          * Notifies all listeners that the insert has made some progress.
140          *
141          * @see InsertListener#projectInsertProgress(Project, int, int, int, int,
142          *      boolean)
143          * @param succeeded
144          *            The number of succeeded blocks
145          * @param failed
146          *            The number of failed blocks
147          * @param fatal
148          *            The number of fatally failed blocks
149          * @param total
150          *            The total number of blocks
151          * @param finalized
152          *            <code>true</code> if the total number of blocks has already
153          *            been finalized, <code>false</code> otherwise
154          */
155         protected void fireProjectInsertProgress(int succeeded, int failed, int fatal, int total, boolean finalized) {
156                 for (InsertListener insertListener : insertListeners) {
157                         insertListener.projectInsertProgress(project, succeeded, failed, fatal, total, finalized);
158                 }
159         }
160
161         /**
162          * Notifies all listeners the project insert has finished.
163          *
164          * @see InsertListener#projectInsertFinished(Project, boolean, Throwable)
165          * @param success
166          *            <code>true</code> if the project was inserted successfully,
167          *            <code>false</code> if it failed
168          * @param cause
169          *            The cause of the failure, if any
170          */
171         protected void fireProjectInsertFinished(boolean success, Throwable cause) {
172                 for (InsertListener insertListener : insertListeners) {
173                         insertListener.projectInsertFinished(project, success, cause);
174                 }
175         }
176
177         /**
178          * Sets the project to insert.
179          *
180          * @param project
181          *            The project to insert
182          */
183         public void setProject(Project project) {
184                 this.project = project;
185         }
186
187         /**
188          * Sets the freenet interface to use.
189          *
190          * @param freenetInterface
191          *            The freenet interface to use
192          */
193         public void setFreenetInterface(Freenet7Interface freenetInterface) {
194                 this.freenetInterface = freenetInterface;
195         }
196
197         /**
198          * Starts the insert.
199          */
200         public void start() {
201                 fileScanner = new FileScanner(project);
202                 fileScanner.addFileScannerListener(this);
203                 new Thread(fileScanner).start();
204         }
205
206         /**
207          * Creates an input stream that delivers the given file, replacing edition
208          * tokens in the file’s content, if necessary.
209          *
210          * @param filename
211          *            The name of the file
212          * @param fileOption
213          *            The file options
214          * @param edition
215          *            The current edition
216          * @param length
217          *            An array containing a single long which is used to
218          *            <em>return</em> the final length of the file, after all
219          *            replacements
220          * @return The input stream for the file
221          * @throws IOException
222          *             if an I/O error occurs
223          */
224         private InputStream createFileInputStream(String filename, FileOption fileOption, int edition, long[] length) throws IOException {
225                 File file = new File(project.getLocalPath(), filename);
226                 length[0] = file.length();
227                 if (!fileOption.getReplaceEdition()) {
228                         return new FileInputStream(file);
229                 }
230                 ByteArrayOutputStream filteredByteOutputStream = new ByteArrayOutputStream(Math.min(Integer.MAX_VALUE, (int) length[0]));
231                 ReplacingOutputStream outputStream = new ReplacingOutputStream(filteredByteOutputStream);
232                 FileInputStream fileInput = new FileInputStream(file);
233                 try {
234                         outputStream.addReplacement("$[EDITION]", String.valueOf(edition));
235                         outputStream.addReplacement("$[URI]", project.getFinalRequestURI(0));
236                         for (int index = 1; index <= fileOption.getEditionRange(); index++) {
237                                 outputStream.addReplacement("$[URI+" + index + "]", project.getFinalRequestURI(index));
238                                 outputStream.addReplacement("$[EDITION+" + index + "]", String.valueOf(edition + index));
239                         }
240                         StreamCopier.copy(fileInput, outputStream, length[0]);
241                 } finally {
242                         Closer.close(fileInput);
243                         Closer.close(outputStream);
244                         Closer.close(filteredByteOutputStream);
245                 }
246                 byte[] filteredBytes = filteredByteOutputStream.toByteArray();
247                 length[0] = filteredBytes.length;
248                 return new ByteArrayInputStream(filteredBytes);
249         }
250
251         /**
252          * Creates an input stream for a container.
253          *
254          * @param containerFiles
255          *            All container definitions
256          * @param containerName
257          *            The name of the container to create
258          * @param edition
259          *            The current edition
260          * @param containerLength
261          *            An array containing a single long which is used to
262          *            <em>return</em> the final length of the container stream,
263          *            after all replacements
264          * @return The input stream for the container
265          * @throws IOException
266          *             if an I/O error occurs
267          */
268         private InputStream createContainerInputStream(Map<String, List<String>> containerFiles, String containerName, int edition, long[] containerLength) throws IOException {
269                 File tempFile = File.createTempFile("jsite", ".zip");
270                 tempFile.deleteOnExit();
271                 FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
272                 ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
273                 try {
274                         for (String filename : containerFiles.get(containerName)) {
275                                 File dataFile = new File(project.getLocalPath(), filename);
276                                 if (dataFile.exists()) {
277                                         ZipEntry zipEntry = new ZipEntry(filename);
278                                         long[] fileLength = new long[1];
279                                         InputStream wrappedInputStream = createFileInputStream(filename, project.getFileOption(filename), edition, fileLength);
280                                         try {
281                                                 zipOutputStream.putNextEntry(zipEntry);
282                                                 StreamCopier.copy(wrappedInputStream, zipOutputStream, fileLength[0]);
283                                         } finally {
284                                                 zipOutputStream.closeEntry();
285                                                 wrappedInputStream.close();
286                                         }
287                                 }
288                         }
289                 } finally {
290                         zipOutputStream.closeEntry();
291                         Closer.close(zipOutputStream);
292                         Closer.close(fileOutputStream);
293                 }
294
295                 containerLength[0] = tempFile.length();
296                 return new FileInputStream(tempFile);
297         }
298
299         /**
300          * Creates a file entry suitable for handing in to
301          * {@link ClientPutComplexDir#addFileEntry(FileEntry)}.
302          *
303          * @param filename
304          *            The name of the file to insert
305          * @param edition
306          *            The current edition
307          * @param containerFiles
308          *            The container definitions
309          * @return A file entry for the given file
310          */
311         private FileEntry createFileEntry(String filename, int edition, Map<String, List<String>> containerFiles) {
312                 FileEntry fileEntry = null;
313                 FileOption fileOption = project.getFileOption(filename);
314                 if (filename.startsWith("/container/:")) {
315                         String containerName = filename.substring("/container/:".length());
316                         try {
317                                 long[] containerLength = new long[1];
318                                 InputStream containerInputStream = createContainerInputStream(containerFiles, containerName, edition, containerLength);
319                                 fileEntry = new DirectFileEntry(containerName + ".zip", "application/zip", containerInputStream, containerLength[0]);
320                         } catch (IOException ioe1) {
321                                 /* ignore, null is returned. */
322                         }
323                 } else {
324                         if (fileOption.isInsert()) {
325                                 try {
326                                         long[] fileLength = new long[1];
327                                         InputStream fileEntryInputStream = createFileInputStream(filename, fileOption, edition, fileLength);
328                                         fileEntry = new DirectFileEntry(filename, project.getFileOption(filename).getMimeType(), fileEntryInputStream, fileLength[0]);
329                                 } catch (IOException ioe1) {
330                                         /* ignore, null is returned. */
331                                 }
332                         } else {
333                                 fileEntry = new RedirectFileEntry(filename, fileOption.getMimeType(), fileOption.getCustomKey());
334                         }
335                 }
336                 return fileEntry;
337         }
338
339         /**
340          * Creates container definitions.
341          *
342          * @param files
343          *            The list of all files
344          * @param containers
345          *            The list of all containers
346          * @param containerFiles
347          *            Empty map that will be filled with container definitions
348          */
349         private void createContainers(List<String> files, List<String> containers, Map<String, List<String>> containerFiles) {
350                 for (String filename : new ArrayList<String>(files)) {
351                         FileOption fileOption = project.getFileOption(filename);
352                         String containerName = fileOption.getContainer();
353                         if (!containerName.equals("")) {
354                                 if (!containers.contains(containerName)) {
355                                         containers.add(containerName);
356                                         containerFiles.put(containerName, new ArrayList<String>());
357                                         /* hmm. looks like a hack to me. */
358                                         files.add("/container/:" + containerName);
359                                 }
360                                 containerFiles.get(containerName).add(filename);
361                                 files.remove(filename);
362                         }
363                 }
364         }
365
366         /**
367          * {@inheritDoc}
368          */
369         public void run() {
370                 fireProjectInsertStarted();
371                 List<String> files = fileScanner.getFiles();
372
373                 /* create connection to node */
374                 Connection connection = freenetInterface.getConnection("project-insert-" + random + counter++);
375                 boolean connected = false;
376                 Throwable cause = null;
377                 try {
378                         connected = connection.connect();
379                 } catch (IOException e1) {
380                         cause = e1;
381                 }
382
383                 if (!connected) {
384                         fireProjectInsertFinished(false, cause);
385                         return;
386                 }
387
388                 Client client = new Client(connection);
389
390                 /* create containers */
391                 final List<String> containers = new ArrayList<String>();
392                 final Map<String, List<String>> containerFiles = new HashMap<String, List<String>>();
393                 createContainers(files, containers, containerFiles);
394
395                 /* collect files */
396                 int edition = project.getEdition();
397                 String dirURI = "USK@" + project.getInsertURI() + "/" + project.getPath() + "/" + edition + "/";
398                 ClientPutComplexDir putDir = new ClientPutComplexDir("dir-" + counter++, dirURI);
399                 if ((project.getIndexFile() != null) && (project.getIndexFile().length() > 0)) {
400                         putDir.setDefaultName(project.getIndexFile());
401                 }
402                 putDir.setVerbosity(Verbosity.ALL);
403                 putDir.setMaxRetries(-1);
404                 for (String filename : files) {
405                         FileEntry fileEntry = createFileEntry(filename, edition, containerFiles);
406                         if (fileEntry != null) {
407                                 putDir.addFileEntry(fileEntry);
408                         }
409                 }
410
411                 /* start request */
412                 try {
413                         client.execute(putDir);
414                 } catch (IOException ioe1) {
415                         fireProjectInsertFinished(false, ioe1);
416                         return;
417                 }
418
419                 /* parse progress and success messages */
420                 String finalURI = null;
421                 boolean firstMessage = true;
422                 boolean success = false;
423                 boolean finished = false;
424                 boolean disconnected = false;
425                 while (!finished) {
426                         Message message = client.readMessage();
427                         finished = (message == null) || (disconnected = client.isDisconnected());
428                         if (firstMessage) {
429                                 fireProjectUploadFinished();
430                                 firstMessage = false;
431                         }
432                         logger.log(Level.FINE, "Received message: " + message);
433                         if (!finished) {
434                                 @SuppressWarnings("null")
435                                 String messageName = message.getName();
436                                 if ("URIGenerated".equals(messageName)) {
437                                         finalURI = message.get("URI");
438                                         fireProjectURIGenerated(finalURI);
439                                 }
440                                 if ("SimpleProgress".equals(messageName)) {
441                                         int total = Integer.parseInt(message.get("Total"));
442                                         int succeeded = Integer.parseInt(message.get("Succeeded"));
443                                         int fatal = Integer.parseInt(message.get("FatallyFailed"));
444                                         int failed = Integer.parseInt(message.get("Failed"));
445                                         boolean finalized = Boolean.parseBoolean(message.get("FinalizedTotal"));
446                                         fireProjectInsertProgress(succeeded, failed, fatal, total, finalized);
447                                 }
448                                 success = "PutSuccessful".equals(messageName);
449                                 finished = success || "PutFailed".equals(messageName) || messageName.endsWith("Error");
450                         }
451                 }
452
453                 /* post-insert work */
454                 fireProjectInsertFinished(success, disconnected ? new IOException("Connection terminated") : null);
455                 if (success) {
456                         @SuppressWarnings("null")
457                         String editionPart = finalURI.substring(finalURI.lastIndexOf('/') + 1);
458                         int newEdition = Integer.parseInt(editionPart);
459                         project.setEdition(newEdition);
460                         project.setLastInsertionTime(System.currentTimeMillis());
461                 }
462         }
463
464         //
465         // INTERFACE FileScannerListener
466         //
467
468         /**
469          * {@inheritDoc}
470          */
471         public void fileScannerFinished(FileScanner fileScanner) {
472                 if (!fileScanner.isError()) {
473                         new Thread(this).start();
474                 } else {
475                         fireProjectInsertFinished(false, null);
476                 }
477                 fileScanner.removeFileScannerListener(this);
478         }
479
480 }