2 * jSite - a tool for uploading websites into Freenet
3 * Copyright (C) 2006 David Roden
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.
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.
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.
20 package de.todesbaum.jsite.application;
22 import java.io.ByteArrayInputStream;
23 import java.io.ByteArrayOutputStream;
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;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35 import java.util.zip.ZipEntry;
36 import java.util.zip.ZipOutputStream;
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;
53 * Manages project inserts.
55 * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
57 public class ProjectInserter implements FileScannerListener, Runnable {
60 private static final Logger logger = Logger.getLogger(ProjectInserter.class.getName());
62 /** Random number for FCP instances. */
63 private static final int random = (int) (Math.random() * Integer.MAX_VALUE);
65 /** Counter for FCP connection identifier. */
66 private static int counter = 0;
68 /** The list of insert listeners. */
69 private List<InsertListener> insertListeners = new ArrayList<InsertListener>();
71 /** The freenet interface. */
72 protected Freenet7Interface freenetInterface;
74 /** The project to insert. */
75 protected Project project;
77 /** The file scanner. */
78 private FileScanner fileScanner;
80 /** Object used for synchronization. */
81 protected final Object lockObject = new Object();
83 /** The temp directory. */
84 private String tempDirectory;
87 * Adds a listener to the list of registered listeners.
89 * @param insertListener
92 public void addInsertListener(InsertListener insertListener) {
93 insertListeners.add(insertListener);
97 * Removes a listener from the list of registered listeners.
99 * @param insertListener
100 * The listener to remove
102 public void removeInsertListener(InsertListener insertListener) {
103 insertListeners.remove(insertListener);
107 * Notifies all listeners that the project insert has started.
109 * @see InsertListener#projectInsertStarted(Project)
111 protected void fireProjectInsertStarted() {
112 for (InsertListener insertListener : insertListeners) {
113 insertListener.projectInsertStarted(project);
118 * Notifies all listeners that the insert has generated a URI.
120 * @see InsertListener#projectURIGenerated(Project, String)
124 protected void fireProjectURIGenerated(String uri) {
125 for (InsertListener insertListener : insertListeners) {
126 insertListener.projectURIGenerated(project, uri);
131 * Notifies all listeners that the insert has made some progress.
133 * @see InsertListener#projectUploadFinished(Project)
135 protected void fireProjectUploadFinished() {
136 for (InsertListener insertListener : insertListeners) {
137 insertListener.projectUploadFinished(project);
142 * Notifies all listeners that the insert has made some progress.
144 * @see InsertListener#projectInsertProgress(Project, int, int, int, int,
147 * The number of succeeded blocks
149 * The number of failed blocks
151 * The number of fatally failed blocks
153 * The total number of blocks
155 * <code>true</code> if the total number of blocks has already
156 * been finalized, <code>false</code> otherwise
158 protected void fireProjectInsertProgress(int succeeded, int failed, int fatal, int total, boolean finalized) {
159 for (InsertListener insertListener : insertListeners) {
160 insertListener.projectInsertProgress(project, succeeded, failed, fatal, total, finalized);
165 * Notifies all listeners the project insert has finished.
167 * @see InsertListener#projectInsertFinished(Project, boolean, Throwable)
169 * <code>true</code> if the project was inserted successfully,
170 * <code>false</code> if it failed
172 * The cause of the failure, if any
174 protected void fireProjectInsertFinished(boolean success, Throwable cause) {
175 for (InsertListener insertListener : insertListeners) {
176 insertListener.projectInsertFinished(project, success, cause);
181 * Sets the project to insert.
184 * The project to insert
186 public void setProject(Project project) {
187 this.project = project;
191 * Sets the freenet interface to use.
193 * @param freenetInterface
194 * The freenet interface to use
196 public void setFreenetInterface(Freenet7Interface freenetInterface) {
197 this.freenetInterface = freenetInterface;
201 * Sets the temp directory to use.
203 * @param tempDirectory
204 * The temp directory to use, or {@code null} to use the system
207 public void setTempDirectory(String tempDirectory) {
208 this.tempDirectory = tempDirectory;
214 public void start() {
215 fileScanner = new FileScanner(project);
216 fileScanner.addFileScannerListener(this);
217 new Thread(fileScanner).start();
221 * Creates an input stream that delivers the given file, replacing edition
222 * tokens in the file’s content, if necessary.
225 * The name of the file
229 * The current edition
231 * An array containing a single long which is used to
232 * <em>return</em> the final length of the file, after all
234 * @return The input stream for the file
235 * @throws IOException
236 * if an I/O error occurs
238 private InputStream createFileInputStream(String filename, FileOption fileOption, int edition, long[] length) throws IOException {
239 File file = new File(project.getLocalPath(), filename);
240 length[0] = file.length();
241 if (!fileOption.getReplaceEdition()) {
242 return new FileInputStream(file);
244 ByteArrayOutputStream filteredByteOutputStream = new ByteArrayOutputStream(Math.min(Integer.MAX_VALUE, (int) length[0]));
245 ReplacingOutputStream outputStream = new ReplacingOutputStream(filteredByteOutputStream);
246 FileInputStream fileInput = new FileInputStream(file);
248 outputStream.addReplacement("$[EDITION]", String.valueOf(edition));
249 outputStream.addReplacement("$[URI]", project.getFinalRequestURI(0));
250 for (int index = 1; index <= fileOption.getEditionRange(); index++) {
251 outputStream.addReplacement("$[URI+" + index + "]", project.getFinalRequestURI(index));
252 outputStream.addReplacement("$[EDITION+" + index + "]", String.valueOf(edition + index));
254 StreamCopier.copy(fileInput, outputStream, length[0]);
256 Closer.close(fileInput);
257 Closer.close(outputStream);
258 Closer.close(filteredByteOutputStream);
260 byte[] filteredBytes = filteredByteOutputStream.toByteArray();
261 length[0] = filteredBytes.length;
262 return new ByteArrayInputStream(filteredBytes);
266 * Creates an input stream for a container.
268 * @param containerFiles
269 * All container definitions
270 * @param containerName
271 * The name of the container to create
273 * The current edition
274 * @param containerLength
275 * An array containing a single long which is used to
276 * <em>return</em> the final length of the container stream,
277 * after all replacements
278 * @return The input stream for the container
279 * @throws IOException
280 * if an I/O error occurs
282 private InputStream createContainerInputStream(Map<String, List<String>> containerFiles, String containerName, int edition, long[] containerLength) throws IOException {
283 File tempFile = File.createTempFile("jsite", ".zip", (tempDirectory == null) ? null : new File(tempDirectory));
284 tempFile.deleteOnExit();
285 FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
286 ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
288 for (String filename : containerFiles.get(containerName)) {
289 File dataFile = new File(project.getLocalPath(), filename);
290 if (dataFile.exists()) {
291 ZipEntry zipEntry = new ZipEntry(filename);
292 long[] fileLength = new long[1];
293 InputStream wrappedInputStream = createFileInputStream(filename, project.getFileOption(filename), edition, fileLength);
295 zipOutputStream.putNextEntry(zipEntry);
296 StreamCopier.copy(wrappedInputStream, zipOutputStream, fileLength[0]);
298 zipOutputStream.closeEntry();
299 wrappedInputStream.close();
304 zipOutputStream.closeEntry();
305 Closer.close(zipOutputStream);
306 Closer.close(fileOutputStream);
309 containerLength[0] = tempFile.length();
310 return new FileInputStream(tempFile);
314 * Creates a file entry suitable for handing in to
315 * {@link ClientPutComplexDir#addFileEntry(FileEntry)}.
318 * The name of the file to insert
320 * The current edition
321 * @param containerFiles
322 * The container definitions
323 * @return A file entry for the given file
325 private FileEntry createFileEntry(String filename, int edition, Map<String, List<String>> containerFiles) {
326 FileEntry fileEntry = null;
327 FileOption fileOption = project.getFileOption(filename);
328 if (filename.startsWith("/container/:")) {
329 String containerName = filename.substring("/container/:".length());
331 long[] containerLength = new long[1];
332 InputStream containerInputStream = createContainerInputStream(containerFiles, containerName, edition, containerLength);
333 fileEntry = new DirectFileEntry(containerName + ".zip", "application/zip", containerInputStream, containerLength[0]);
334 } catch (IOException ioe1) {
335 /* ignore, null is returned. */
338 if (fileOption.isInsert()) {
340 long[] fileLength = new long[1];
341 InputStream fileEntryInputStream = createFileInputStream(filename, fileOption, edition, fileLength);
342 fileEntry = new DirectFileEntry(filename, project.getFileOption(filename).getMimeType(), fileEntryInputStream, fileLength[0]);
343 } catch (IOException ioe1) {
344 /* ignore, null is returned. */
347 fileEntry = new RedirectFileEntry(filename, fileOption.getMimeType(), fileOption.getCustomKey());
354 * Creates container definitions.
357 * The list of all files
359 * The list of all containers
360 * @param containerFiles
361 * Empty map that will be filled with container definitions
363 private void createContainers(List<String> files, List<String> containers, Map<String, List<String>> containerFiles) {
364 for (String filename : new ArrayList<String>(files)) {
365 FileOption fileOption = project.getFileOption(filename);
366 String containerName = fileOption.getContainer();
367 if (!containerName.equals("")) {
368 if (!containers.contains(containerName)) {
369 containers.add(containerName);
370 containerFiles.put(containerName, new ArrayList<String>());
371 /* hmm. looks like a hack to me. */
372 files.add("/container/:" + containerName);
374 containerFiles.get(containerName).add(filename);
375 files.remove(filename);
384 fireProjectInsertStarted();
385 List<String> files = fileScanner.getFiles();
387 /* create connection to node */
388 Connection connection = freenetInterface.getConnection("project-insert-" + random + counter++);
389 connection.setTempDirectory(tempDirectory);
390 boolean connected = false;
391 Throwable cause = null;
393 connected = connection.connect();
394 } catch (IOException e1) {
399 fireProjectInsertFinished(false, cause);
403 Client client = new Client(connection);
405 /* create containers */
406 final List<String> containers = new ArrayList<String>();
407 final Map<String, List<String>> containerFiles = new HashMap<String, List<String>>();
408 createContainers(files, containers, containerFiles);
411 int edition = project.getEdition();
412 String dirURI = "USK@" + project.getInsertURI() + "/" + project.getPath() + "/" + edition + "/";
413 ClientPutComplexDir putDir = new ClientPutComplexDir("dir-" + counter++, dirURI, tempDirectory);
414 if ((project.getIndexFile() != null) && (project.getIndexFile().length() > 0)) {
415 putDir.setDefaultName(project.getIndexFile());
417 putDir.setVerbosity(Verbosity.ALL);
418 putDir.setMaxRetries(-1);
419 for (String filename : files) {
420 FileEntry fileEntry = createFileEntry(filename, edition, containerFiles);
421 if (fileEntry != null) {
423 putDir.addFileEntry(fileEntry);
424 } catch (IOException ioe1) {
425 fireProjectInsertFinished(false, ioe1);
433 client.execute(putDir);
434 } catch (IOException ioe1) {
435 fireProjectInsertFinished(false, ioe1);
439 /* parse progress and success messages */
440 String finalURI = null;
441 boolean firstMessage = true;
442 boolean success = false;
443 boolean finished = false;
444 boolean disconnected = false;
446 Message message = client.readMessage();
447 finished = (message == null) || (disconnected = client.isDisconnected());
449 fireProjectUploadFinished();
450 firstMessage = false;
452 logger.log(Level.FINE, "Received message: " + message);
454 @SuppressWarnings("null")
455 String messageName = message.getName();
456 if ("URIGenerated".equals(messageName)) {
457 finalURI = message.get("URI");
458 fireProjectURIGenerated(finalURI);
460 if ("SimpleProgress".equals(messageName)) {
461 int total = Integer.parseInt(message.get("Total"));
462 int succeeded = Integer.parseInt(message.get("Succeeded"));
463 int fatal = Integer.parseInt(message.get("FatallyFailed"));
464 int failed = Integer.parseInt(message.get("Failed"));
465 boolean finalized = Boolean.parseBoolean(message.get("FinalizedTotal"));
466 fireProjectInsertProgress(succeeded, failed, fatal, total, finalized);
468 success = "PutSuccessful".equals(messageName);
469 finished = success || "PutFailed".equals(messageName) || messageName.endsWith("Error");
473 /* post-insert work */
474 fireProjectInsertFinished(success, disconnected ? new IOException("Connection terminated") : null);
476 @SuppressWarnings("null")
477 String editionPart = finalURI.substring(finalURI.lastIndexOf('/') + 1);
478 int newEdition = Integer.parseInt(editionPart);
479 project.setEdition(newEdition);
480 project.setLastInsertionTime(System.currentTimeMillis());
485 // INTERFACE FileScannerListener
491 public void fileScannerFinished(FileScanner fileScanner) {
492 if (!fileScanner.isError()) {
493 new Thread(this).start();
495 fireProjectInsertFinished(false, null);
497 fileScanner.removeFileScannerListener(this);