/* * Copyright 2017 - 2020 Acosix GmbH * * 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 de.acosix.alfresco.simplecontentstores.repo.store.facade; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.List; import org.alfresco.repo.content.filestore.FileContentWriter; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentStreamListener; import org.alfresco.util.TempFileProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.FileCopyUtils; /** * @author Axel Faust */ public class ContentReaderFacade extends ContentAccessorFacade<ContentReader> implements ContentReader { private static final Logger LOGGER = LoggerFactory.getLogger(ContentReaderFacade.class); protected final List<ContentStreamListener> listeners = new ArrayList<>(); public ContentReaderFacade(final ContentReader delegate) { super(delegate); } protected ContentReaderFacade() { super(); } /** * {@inheritDoc} */ @Override public ContentReader getReader() throws ContentIOException { this.ensureDelegate(); return this.delegate.getReader(); } /** * {@inheritDoc} */ @Override public boolean exists() { this.ensureDelegate(); return this.delegate.exists(); } /** * {@inheritDoc} */ @Override public long getLastModified() { this.ensureDelegate(); return this.delegate.getLastModified(); } /** * {@inheritDoc} */ @Override public boolean isClosed() { this.ensureDelegate(); return this.delegate.isClosed(); } /** * {@inheritDoc} */ @Override public ReadableByteChannel getReadableChannel() throws ContentIOException { this.ensureDelegate(); return this.delegate.getReadableChannel(); } /** * {@inheritDoc} */ @Override public FileChannel getFileChannel() throws ContentIOException { final ReadableByteChannel readableChannel = this.getReadableChannel(); final FileChannel fileChannel; // since specific facade sub-classes may adapt getReadableChannel to facade the channel from delegate we should handle file channel // support here for consistency (else we would have to implement this in each sub-class) // the following has been taken from AbstractContentReader to spoof FileChannel (with various modifications) if (readableChannel instanceof FileChannel) { fileChannel = (FileChannel) readableChannel; LOGGER.debug("Content reader {} provided direct support for FileChannel", this); } else { final File tempFile = TempFileProvider.createTempFile("random_read_spoof_", ".bin"); final FileContentWriter spoofWriter = new FileContentWriter(tempFile); final FileChannel spoofWriterChannel = spoofWriter.getFileChannel(false); try { final long spoofFileSize = this.getSize(); spoofWriterChannel.transferFrom(readableChannel, 0, spoofFileSize); LOGGER.debug("Content reader {} copied content to enable random access", this); } catch (final IOException e) { LOGGER.error("Content reader {} failed to copy content to enable random access", this, e); throw new ContentIOException("Failed to copy from permanent channel to spoofed temporary channel: \n\treader: " + this + "\n\ttemp: " + spoofWriter, e); } finally { try { spoofWriterChannel.close(); } catch (final IOException e) { LOGGER.debug("Error closing spoofed writer channel", e); } } final ContentReader spoofReader = spoofWriter.getReader(); final ContentStreamListener spoofListener = () -> { try { readableChannel.close(); } catch (final IOException e) { throw new ContentIOException("Failed to close underlying channel", e); } }; spoofReader.addListener(spoofListener); fileChannel = spoofReader.getFileChannel(); LOGGER.debug("Content reader {} provided indirect support for FileChannel via {}", this, spoofWriter); } return fileChannel; } /** * {@inheritDoc} */ @Override public InputStream getContentInputStream() throws ContentIOException { try { final ReadableByteChannel channel = this.getReadableChannel(); InputStream is = Channels.newInputStream(channel); is = new BufferedInputStream(is); return is; } catch (final Throwable e) { LOGGER.error("Failed to open stream onto channel for reader {}", this, e); throw new ContentIOException("Failed to open stream onto channel: \n accessor: " + this, e); } } /** * {@inheritDoc} */ @Override // same implementation as AbstractContentReader public void getContent(final OutputStream os) throws ContentIOException { try { final InputStream is = this.getContentInputStream(); FileCopyUtils.copy(is, os); } catch (final IOException e) { throw new ContentIOException("Failed to copy content to output stream: \n" + " accessor: " + this, e); } } /** * {@inheritDoc} */ @Override // overriden with same implementation as AbstractContentReader public void getContent(final File file) throws ContentIOException { try { final InputStream is = this.getContentInputStream(); final FileOutputStream os = new FileOutputStream(file); FileCopyUtils.copy(is, os); } catch (final IOException e) { throw new ContentIOException("Failed to copy content to file: \n" + " accessor: " + this + "\n" + " file: " + file, e); } } /** * {@inheritDoc} */ @Override // same implementation as AbstractContentReader public String getContentString() throws ContentIOException { try { // read from the stream into a byte[] final InputStream is = this.getContentInputStream(); final ByteArrayOutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(is, os); // both streams are closed final byte[] bytes = os.toByteArray(); // get the encoding for the string final String encoding = this.getEncoding(); // create the string from the byte[] using encoding if necessary final String systemEncoding = System.getProperty("file.encoding"); final String content = (encoding == null) ? new String(bytes, systemEncoding) : new String(bytes, encoding); // done return content; } catch (final IOException e) { throw new ContentIOException("Failed to copy content to string: \n" + " accessor: " + this, e); } } /** * {@inheritDoc} */ @Override // same implementation as AbstractContentReader public String getContentString(final int length) throws ContentIOException { if (length <= 0) { throw new IllegalArgumentException("Character count must be positive and within range"); } Reader reader = null; try { // just create buffer of the required size final char[] buffer = new char[length]; final String encoding = this.getEncoding(); // create a reader from the input stream if (encoding == null) { final String systemEncoding = System.getProperty("file.encoding"); reader = new InputStreamReader(this.getContentInputStream(), systemEncoding); } else { reader = new InputStreamReader(this.getContentInputStream(), encoding); } // read it all, if possible final int count = reader.read(buffer, 0, length); // there may have been fewer characters - create a new string as the result return (count != -1 ? new String(buffer, 0, count) : ""); } catch (final IOException e) { throw new ContentIOException("Failed to copy content to string: \n\taccessor: " + this + "\n\tlength: " + length, e); } finally { if (reader != null) { try { reader.close(); } catch (final Throwable e) { LOGGER.debug("Failed to close reader", e); } } } } }