/******************************************************************************* * * Copyright (c) 2004-2009 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi * * *******************************************************************************/ package hudson.model; import hudson.util.ByteBuffer; import hudson.util.CharSpool; import hudson.util.LineEndNormalizingWriter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.framework.io.WriterOutputStream; import org.apache.commons.io.output.CountingOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.io.Writer; import java.io.Reader; import java.io.InputStreamReader; /** * Represents a large text data. * * <p> This class defines methods for handling progressive text update. * * @author Kohsuke Kawaguchi * @deprecated moved to stapler, as of Hudson 1.220 */ public class LargeText { /** * Represents the data source of this text. */ private interface Source { Session open() throws IOException; long length(); boolean exists(); } private final Source source; private volatile boolean completed; public LargeText(final File file, boolean completed) { this.source = new Source() { public Session open() throws IOException { return new FileSession(file); } public long length() { return file.length(); } public boolean exists() { return file.exists(); } }; this.completed = completed; } public LargeText(final ByteBuffer memory, boolean completed) { this.source = new Source() { public Session open() throws IOException { return new BufferSession(memory); } public long length() { return memory.length(); } public boolean exists() { return true; } }; this.completed = completed; } public void markAsComplete() { completed = true; } public boolean isComplete() { return completed; } /** * Returns {@link Reader} for reading the raw bytes. */ public Reader readAll() throws IOException { return new InputStreamReader(new InputStream() { final Session session = source.open(); public int read() throws IOException { byte[] buf = new byte[1]; int n = session.read(buf); if (n == 1) { return buf[0]; } else { return -1; // EOF } } public int read(byte[] buf, int off, int len) throws IOException { return session.read(buf, off, len); } public void close() throws IOException { session.close(); } }); } /** * Writes the tail portion of the file to the {@link Writer}. * * <p> The text file is assumed to be in the system default encoding. * * @param start The byte offset in the input file where the write operation * starts. * * @return if the file is still being written, this method writes the file * until the last newline character and returns the offset to start the next * write operation. */ public long writeLogTo(long start, Writer w) throws IOException { CountingOutputStream os = new CountingOutputStream(new WriterOutputStream(w)); Session f = source.open(); f.skip(start); if (completed) { // write everything till EOF byte[] buf = new byte[1024]; int sz; while ((sz = f.read(buf)) >= 0) { os.write(buf, 0, sz); } } else { ByteBuf buf = new ByteBuf(null, f); HeadMark head = new HeadMark(buf); TailMark tail = new TailMark(buf); while (tail.moveToNextLine(f)) { head.moveTo(tail, os); } head.finish(os); } f.close(); os.flush(); return os.getCount() + start; } /** * Implements the progressive text handling. This method is used as a "web * method" with progressiveText.jelly. */ public void doProgressText(StaplerRequest req, StaplerResponse rsp) throws IOException { rsp.setContentType("text/plain"); rsp.setStatus(HttpServletResponse.SC_OK); if (!source.exists()) { // file doesn't exist yet rsp.addHeader("X-Text-Size", "0"); rsp.addHeader("X-More-Data", "true"); return; } long start = 0; String s = req.getParameter("start"); if (s != null) { start = Long.parseLong(s); } if (source.length() < start) { start = 0; // text rolled over } CharSpool spool = new CharSpool(); long r = writeLogTo(start, spool); rsp.addHeader("X-Text-Size", String.valueOf(r)); if (!completed) { rsp.addHeader("X-More-Data", "true"); } // when sending big text, try compression. don't bother if it's small Writer w; if (r - start > 4096) { w = rsp.getCompressedWriter(req); } else { w = rsp.getWriter(); } spool.writeTo(new LineEndNormalizingWriter(w)); w.close(); } /** * Points to a byte in the buffer. */ private static class Mark { protected ByteBuf buf; protected int pos; public Mark(ByteBuf buf) { this.buf = buf; } } /** * Points to the start of the region that's not committed to the output yet. */ private static final class HeadMark extends Mark { public HeadMark(ByteBuf buf) { super(buf); } /** * Moves this mark to 'that' mark, and writes the data to * {@link OutputStream} if necessary. */ void moveTo(Mark that, OutputStream os) throws IOException { while (this.buf != that.buf) { os.write(buf.buf, 0, buf.size); buf = buf.next; pos = 0; } this.pos = that.pos; } void finish(OutputStream os) throws IOException { os.write(buf.buf, 0, pos); } } /** * Points to the end of the region. */ private static final class TailMark extends Mark { public TailMark(ByteBuf buf) { super(buf); } boolean moveToNextLine(Session f) throws IOException { while (true) { while (pos == buf.size) { if (!buf.isFull()) { // read until EOF return false; } else { // read into the next buffer buf = new ByteBuf(buf, f); pos = 0; } } byte b = buf.buf[pos++]; if (b == '\r' || b == '\n') { return true; } } } } private static final class ByteBuf { private final byte[] buf = new byte[1024]; private int size = 0; private ByteBuf next; public ByteBuf(ByteBuf previous, Session f) throws IOException { if (previous != null) { assert previous.next == null; previous.next = this; } while (!this.isFull()) { int chunk = f.read(buf, size, buf.length - size); if (chunk == -1) { return; } size += chunk; } } public boolean isFull() { return buf.length == size; } } /** * Represents the read session of the {@link Source}. Methods generally * follow the contracts of {@link InputStream}. */ private interface Session { void close() throws IOException; void skip(long start) throws IOException; int read(byte[] buf) throws IOException; int read(byte[] buf, int offset, int length) throws IOException; } /** * {@link Session} implementation over {@link RandomAccessFile}. */ private static final class FileSession implements Session { private final RandomAccessFile file; public FileSession(File file) throws IOException { this.file = new RandomAccessFile(file, "r"); } public void close() throws IOException { file.close(); } public void skip(long start) throws IOException { file.seek(file.getFilePointer() + start); } public int read(byte[] buf) throws IOException { return file.read(buf); } public int read(byte[] buf, int offset, int length) throws IOException { return file.read(buf, offset, length); } } /** * {@link Session} implementation over {@link ByteBuffer}. */ private static final class BufferSession implements Session { private final InputStream in; public BufferSession(ByteBuffer buf) { this.in = buf.newInputStream(); } public void close() throws IOException { in.close(); } public void skip(long n) throws IOException { while (n > 0) { n -= in.skip(n); } } public int read(byte[] buf) throws IOException { return in.read(buf); } public int read(byte[] buf, int offset, int length) throws IOException { return in.read(buf, offset, length); } } }