| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.cxf.io; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.CharArrayReader; |
| import java.io.CharArrayWriter; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.Reader; |
| import java.io.Writer; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.security.GeneralSecurityException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import javax.crypto.CipherInputStream; |
| import javax.crypto.CipherOutputStream; |
| |
| import org.apache.cxf.Bus; |
| import org.apache.cxf.BusFactory; |
| import org.apache.cxf.common.util.SystemPropertyAction; |
| import org.apache.cxf.helpers.FileUtils; |
| import org.apache.cxf.helpers.IOUtils; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| public class CachedWriter extends Writer { |
| private static final File DEFAULT_TEMP_DIR; |
| private static int defaultThreshold; |
| private static long defaultMaxSize; |
| private static String defaultCipherTransformation; |
| |
| static { |
| |
| String s = SystemPropertyAction.getPropertyOrNull(CachedConstants.OUTPUT_DIRECTORY_SYS_PROP); |
| if (s == null) { |
| // lookup the deprecated property |
| s = SystemPropertyAction.getPropertyOrNull("org.apache.cxf.io.CachedWriter.OutputDirectory"); |
| } |
| if (s != null) { |
| File f = new File(s); |
| if (f.exists() && f.isDirectory()) { |
| DEFAULT_TEMP_DIR = f; |
| } else { |
| DEFAULT_TEMP_DIR = null; |
| } |
| } else { |
| DEFAULT_TEMP_DIR = null; |
| } |
| |
| setDefaultThreshold(-1); |
| setDefaultMaxSize(-1); |
| setDefaultCipherTransformation(null); |
| } |
| |
| protected boolean outputLocked; |
| protected Writer currentStream; |
| |
| private boolean cosClosed; |
| private long threshold = defaultThreshold; |
| private long maxSize = defaultMaxSize; |
| private File outputDir = DEFAULT_TEMP_DIR; |
| private String cipherTransformation = defaultCipherTransformation; |
| |
| private long totalLength; |
| |
| private boolean inmem; |
| |
| private boolean tempFileFailed; |
| private File tempFile; |
| private boolean allowDeleteOfFile = true; |
| private CipherPair ciphers; |
| |
| private List<CachedWriterCallback> callbacks; |
| |
| private List<Object> streamList = new ArrayList<>(); |
| |
| |
| static class LoadingCharArrayWriter extends CharArrayWriter { |
| LoadingCharArrayWriter() { |
| super(1024); |
| } |
| public char[] rawCharArray() { |
| return super.buf; |
| } |
| } |
| |
| |
| public CachedWriter() { |
| this(defaultThreshold); |
| |
| inmem = true; |
| } |
| |
| public CachedWriter(long threshold) { |
| this.threshold = threshold; |
| currentStream = new LoadingCharArrayWriter(); |
| inmem = true; |
| readBusProperties(); |
| } |
| |
| private void readBusProperties() { |
| Bus b = BusFactory.getThreadDefaultBus(false); |
| if (b != null) { |
| String v = getBusProperty(b, CachedConstants.THRESHOLD_BUS_PROP, null); |
| if (v != null && threshold == defaultThreshold) { |
| threshold = Integer.parseInt(v); |
| } |
| v = getBusProperty(b, CachedConstants.MAX_SIZE_BUS_PROP, null); |
| if (v != null) { |
| maxSize = Integer.parseInt(v); |
| } |
| v = getBusProperty(b, CachedConstants.CIPHER_TRANSFORMATION_BUS_PROP, null); |
| if (v != null) { |
| cipherTransformation = v; |
| } |
| v = getBusProperty(b, CachedConstants.OUTPUT_DIRECTORY_BUS_PROP, null); |
| if (v != null) { |
| File f = new File(v); |
| if (f.exists() && f.isDirectory()) { |
| outputDir = f; |
| } |
| } |
| } |
| } |
| |
| private static String getBusProperty(Bus b, String key, String dflt) { |
| String v = (String)b.getProperty(key); |
| return v != null ? v : dflt; |
| } |
| |
| public void holdTempFile() { |
| allowDeleteOfFile = false; |
| } |
| public void releaseTempFileHold() { |
| allowDeleteOfFile = true; |
| } |
| |
| public void registerCallback(CachedWriterCallback cb) { |
| if (null == callbacks) { |
| callbacks = new ArrayList<>(); |
| } |
| callbacks.add(cb); |
| } |
| |
| public void deregisterCallback(CachedWriterCallback cb) { |
| if (null != callbacks) { |
| callbacks.remove(cb); |
| } |
| } |
| |
| public List<CachedWriterCallback> getCallbacks() { |
| return callbacks == null ? null : Collections.unmodifiableList(callbacks); |
| } |
| |
| /** |
| * Perform any actions required on stream flush (freeze headers, reset |
| * output stream ... etc.) |
| */ |
| protected void doFlush() throws IOException { |
| |
| } |
| |
| public void flush() throws IOException { |
| if (!cosClosed) { |
| currentStream.flush(); |
| } |
| |
| if (null != callbacks) { |
| for (CachedWriterCallback cb : callbacks) { |
| cb.onFlush(this); |
| } |
| } |
| doFlush(); |
| } |
| |
| /** |
| * Perform any actions required on stream closure (handle response etc.) |
| */ |
| protected void doClose() throws IOException { |
| |
| } |
| |
| /** |
| * Perform any actions required after stream closure (close the other related stream etc.) |
| */ |
| protected void postClose() throws IOException { |
| |
| } |
| |
| /** |
| * Locks the output stream to prevent additional writes, but maintains |
| * a pointer to it so an InputStream can be obtained |
| * @throws IOException |
| */ |
| public void lockOutputStream() throws IOException { |
| if (outputLocked) { |
| return; |
| } |
| currentStream.flush(); |
| outputLocked = true; |
| if (null != callbacks) { |
| for (CachedWriterCallback cb : callbacks) { |
| cb.onClose(this); |
| } |
| } |
| doClose(); |
| streamList.remove(currentStream); |
| } |
| |
| public void close() throws IOException { |
| if (!cosClosed) { |
| currentStream.flush(); |
| } |
| outputLocked = true; |
| if (null != callbacks) { |
| for (CachedWriterCallback cb : callbacks) { |
| cb.onClose(this); |
| } |
| } |
| doClose(); |
| currentStream.close(); |
| maybeDeleteTempFile(currentStream); |
| if (ciphers != null) { |
| ciphers.clean(); |
| } |
| postClose(); |
| } |
| |
| public boolean equals(Object obj) { |
| if (obj == this) { |
| return true; |
| } |
| if (obj instanceof CachedWriter) { |
| return currentStream.equals(((CachedWriter)obj).currentStream); |
| } |
| return currentStream.equals(obj); |
| } |
| |
| /** |
| * Replace the original stream with the new one, optionally copying the content of the old one |
| * into the new one. |
| * When with Attachment, needs to replace the xml writer stream with the stream used by |
| * AttachmentSerializer or copy the cached output stream to the "real" |
| * output stream, i.e. onto the wire. |
| * |
| * @param out the new output stream |
| * @param copyOldContent flag indicating if the old content should be copied |
| * @throws IOException |
| */ |
| public void resetOut(Writer out, boolean copyOldContent) throws IOException { |
| if (out == null) { |
| out = new LoadingCharArrayWriter(); |
| } |
| |
| if (currentStream instanceof CachedWriter) { |
| CachedWriter ac = (CachedWriter) currentStream; |
| Reader in = ac.getReader(); |
| IOUtils.copyAndCloseInput(in, out); |
| } else { |
| if (inmem) { |
| if (currentStream instanceof LoadingCharArrayWriter) { |
| LoadingCharArrayWriter byteOut = (LoadingCharArrayWriter) currentStream; |
| if (copyOldContent && byteOut.size() > 0) { |
| byteOut.writeTo(out); |
| } |
| } else { |
| throw new IOException("Unknown format of currentStream"); |
| } |
| } else { |
| // read the file |
| currentStream.close(); |
| if (copyOldContent) { |
| InputStreamReader fin = createInputStreamReader(tempFile); |
| IOUtils.copyAndCloseInput(fin, out); |
| } |
| streamList.remove(currentStream); |
| deleteTempFile(); |
| inmem = true; |
| } |
| } |
| currentStream = out; |
| outputLocked = false; |
| } |
| |
| |
| public long size() { |
| return totalLength; |
| } |
| |
| public char[] getChars() throws IOException { |
| flush(); |
| if (inmem) { |
| if (currentStream instanceof LoadingCharArrayWriter) { |
| return ((LoadingCharArrayWriter)currentStream).toCharArray(); |
| } |
| throw new IOException("Unknown format of currentStream"); |
| } |
| // read the file |
| try (Reader fin = createInputStreamReader(tempFile)) { |
| CharArrayWriter out = new CharArrayWriter((int)tempFile.length()); |
| char[] bytes = new char[1024]; |
| int x = fin.read(bytes); |
| while (x != -1) { |
| out.write(bytes, 0, x); |
| x = fin.read(bytes); |
| } |
| return out.toCharArray(); |
| } |
| } |
| |
| public void writeCacheTo(Writer out) throws IOException { |
| flush(); |
| if (inmem) { |
| if (currentStream instanceof LoadingCharArrayWriter) { |
| ((LoadingCharArrayWriter)currentStream).writeTo(out); |
| } else { |
| throw new IOException("Unknown format of currentStream"); |
| } |
| } else { |
| // read the file |
| try (Reader fin = createInputStreamReader(tempFile)) { |
| char[] bytes = new char[1024]; |
| int x = fin.read(bytes); |
| while (x != -1) { |
| out.write(bytes, 0, x); |
| x = fin.read(bytes); |
| } |
| } |
| } |
| } |
| |
| public void writeCacheTo(StringBuilder out, long limit) throws IOException { |
| flush(); |
| if (totalLength < limit |
| || limit == -1) { |
| writeCacheTo(out); |
| return; |
| } |
| |
| long count = 0; |
| if (inmem) { |
| if (currentStream instanceof LoadingCharArrayWriter) { |
| LoadingCharArrayWriter s = (LoadingCharArrayWriter)currentStream; |
| out.append(s.rawCharArray(), 0, (int)limit); |
| } else { |
| throw new IOException("Unknown format of currentStream"); |
| } |
| } else { |
| // read the file |
| try (Reader fin = createInputStreamReader(tempFile)) { |
| char[] bytes = new char[1024]; |
| long x = fin.read(bytes); |
| while (x != -1) { |
| if ((count + x) > limit) { |
| x = limit - count; |
| } |
| out.append(bytes, 0, (int)x); |
| count += x; |
| |
| if (count >= limit) { |
| x = -1; |
| } else { |
| x = fin.read(bytes); |
| } |
| } |
| } |
| } |
| } |
| |
| public void writeCacheTo(StringBuilder out) throws IOException { |
| flush(); |
| if (inmem) { |
| if (currentStream instanceof LoadingCharArrayWriter) { |
| LoadingCharArrayWriter lcaw = (LoadingCharArrayWriter)currentStream; |
| out.append(lcaw.rawCharArray(), 0, lcaw.size()); |
| } else { |
| throw new IOException("Unknown format of currentStream"); |
| } |
| } else { |
| // read the file |
| try (Reader r = createInputStreamReader(tempFile)) { |
| char[] chars = new char[1024]; |
| int x = r.read(chars); |
| while (x != -1) { |
| out.append(chars, 0, x); |
| x = r.read(chars); |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * @return the underlying output stream |
| */ |
| public Writer getOut() { |
| return currentStream; |
| } |
| |
| public int hashCode() { |
| return currentStream.hashCode(); |
| } |
| |
| public String toString() { |
| StringBuilder builder = new StringBuilder().append('[') |
| .append(CachedWriter.class.getName()) |
| .append(" Content: "); |
| try { |
| writeCacheTo(builder); |
| } catch (IOException e) { |
| //ignore |
| } |
| return builder.append(']').toString(); |
| } |
| |
| protected void onWrite() throws IOException { |
| |
| } |
| |
| private void enforceLimits() throws IOException { |
| if (maxSize > 0 && totalLength > maxSize) { |
| throw new CacheSizeExceededException(); |
| } |
| if (inmem && totalLength > threshold && currentStream instanceof LoadingCharArrayWriter) { |
| createFileOutputStream(); |
| } |
| } |
| |
| |
| public void write(char[] cbuf, int off, int len) throws IOException { |
| if (!outputLocked) { |
| onWrite(); |
| this.totalLength += len; |
| enforceLimits(); |
| currentStream.write(cbuf, off, len); |
| } |
| } |
| |
| private void createFileOutputStream() throws IOException { |
| if (tempFileFailed) { |
| return; |
| } |
| LoadingCharArrayWriter bout = (LoadingCharArrayWriter)currentStream; |
| try { |
| if (outputDir == null) { |
| tempFile = FileUtils.createTempFile("cos", "tmp"); |
| } else { |
| tempFile = FileUtils.createTempFile("cos", "tmp", outputDir, false); |
| } |
| currentStream = createOutputStreamWriter(tempFile); |
| bout.writeTo(currentStream); |
| inmem = false; |
| streamList.add(currentStream); |
| } catch (Exception ex) { |
| //Could be IOException or SecurityException or other issues. |
| //Don't care what, just keep it in memory. |
| tempFileFailed = true; |
| if (currentStream != bout) { |
| currentStream.close(); |
| } |
| deleteTempFile(); |
| inmem = true; |
| currentStream = bout; |
| } |
| } |
| |
| public File getTempFile() { |
| return tempFile != null && tempFile.exists() ? tempFile : null; |
| } |
| |
| public Reader getReader() throws IOException { |
| flush(); |
| if (inmem) { |
| if (currentStream instanceof LoadingCharArrayWriter) { |
| LoadingCharArrayWriter lcaw = (LoadingCharArrayWriter)currentStream; |
| return new CharArrayReader(lcaw.rawCharArray(), 0, lcaw.size()); |
| } |
| return null; |
| } |
| try { |
| InputStream fileInputStream = new FileInputStream(tempFile) { |
| boolean closed; |
| |
| @Override |
| public void close() throws IOException { |
| if (!closed) { |
| super.close(); |
| maybeDeleteTempFile(this); |
| } |
| closed = true; |
| } |
| }; |
| streamList.add(fileInputStream); |
| if (cipherTransformation != null) { |
| fileInputStream = new CipherInputStream(fileInputStream, ciphers.getDecryptor()) { |
| boolean closed; |
| |
| @Override |
| public void close() throws IOException { |
| if (!closed) { |
| super.close(); |
| closed = true; |
| } |
| } |
| }; |
| } |
| return new InputStreamReader(fileInputStream, StandardCharsets.UTF_8); |
| } catch (FileNotFoundException e) { |
| throw new IOException("Cached file was deleted, " + e.toString()); |
| } |
| } |
| |
| private synchronized void deleteTempFile() { |
| if (tempFile != null) { |
| File file = tempFile; |
| tempFile = null; |
| FileUtils.delete(file); |
| } |
| } |
| private void maybeDeleteTempFile(Object stream) { |
| streamList.remove(stream); |
| if (!inmem && tempFile != null && streamList.isEmpty() && allowDeleteOfFile) { |
| if (currentStream != null) { |
| try { |
| currentStream.close(); |
| postClose(); |
| } catch (Exception e) { |
| //ignore |
| } |
| } |
| deleteTempFile(); |
| currentStream = new LoadingCharArrayWriter(); |
| inmem = true; |
| } |
| } |
| |
| public void setOutputDir(File outputDir) throws IOException { |
| this.outputDir = outputDir; |
| } |
| public void setThreshold(long threshold) { |
| this.threshold = threshold; |
| } |
| |
| public void setMaxSize(long maxSize) { |
| this.maxSize = maxSize; |
| } |
| |
| public void setCipherTransformation(String cipherTransformation) { |
| this.cipherTransformation = cipherTransformation; |
| } |
| |
| public static void setDefaultMaxSize(long l) { |
| if (l == -1) { |
| String s = System.getProperty(CachedConstants.MAX_SIZE_SYS_PROP); |
| if (s == null) { |
| // lookup the deprecated property |
| s = System.getProperty("org.apache.cxf.io.CachedWriter.MaxSize", "-1"); |
| } |
| l = Long.parseLong(s); |
| } |
| defaultMaxSize = l; |
| } |
| |
| public static void setDefaultThreshold(int i) { |
| if (i == -1) { |
| i = SystemPropertyAction.getInteger(CachedConstants.THRESHOLD_SYS_PROP, -1); |
| if (i == -1) { |
| // lookup the deprecated property |
| i = SystemPropertyAction.getInteger("org.apache.cxf.io.CachedWriter.Threshold", -1); |
| } |
| if (i <= 0) { |
| i = 64 * 1024; |
| } |
| } |
| defaultThreshold = i; |
| |
| } |
| |
| public static void setDefaultCipherTransformation(String n) { |
| if (n == null) { |
| n = SystemPropertyAction.getPropertyOrNull(CachedConstants.CIPHER_TRANSFORMATION_SYS_PROP); |
| } |
| defaultCipherTransformation = n; |
| } |
| |
| private OutputStreamWriter createOutputStreamWriter(File file) throws IOException { |
| OutputStream out = new BufferedOutputStream(Files.newOutputStream(file.toPath())); |
| if (cipherTransformation != null) { |
| try { |
| if (ciphers == null) { |
| ciphers = new CipherPair(cipherTransformation); |
| } |
| } catch (GeneralSecurityException e) { |
| out.close(); |
| throw new IOException(e.getMessage(), e); |
| } |
| out = new CipherOutputStream(out, ciphers.getEncryptor()) { |
| |
| @Override |
| public void close() throws IOException { |
| if (!cosClosed) { |
| super.close(); |
| cosClosed = true; |
| } |
| } |
| }; |
| } |
| return new OutputStreamWriter(out, UTF_8) { |
| |
| @Override |
| public void close() throws IOException { |
| if (!cosClosed) { |
| super.close(); |
| cosClosed = true; |
| } |
| } |
| }; |
| } |
| |
| private InputStreamReader createInputStreamReader(File file) throws IOException { |
| InputStream in = Files.newInputStream(file.toPath()); |
| if (cipherTransformation != null) { |
| in = new CipherInputStream(in, ciphers.getDecryptor()) { |
| boolean closed; |
| |
| @Override |
| public void close() throws IOException { |
| if (!closed) { |
| super.close(); |
| closed = true; |
| } |
| } |
| }; |
| } |
| return new InputStreamReader(in, UTF_8); |
| } |
| |
| } |