DirStream.java
/*
** Module : DirStream.java
** Abstract : Java implementation of the INPUT FROM OS-DIR Stream
**
** Copyright (c) 2013-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 CS 20130103 Created initial version for Implement OS-DIR support as an INPUT stream task.
** 002 CS 20130325 Changed to handle different behavior from Windows to other OS.
** 003 ECF 20150715 Replace StringBuffer with StringBuilder.
** 004 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 005 EVK 20140601 Remove unnecessary NEW LINE from directory list.
** 006 OM 20170205 Added required NEW LINE in directory list for Windows OS.
** 007 OM 20170622 Implemented peekCh() method.
** 008 ECF 20171026 Added write(byte[], int, int) method.
** 009 OM 20210302 Replaced [CharsetConverter] with [Charset] which supports multibyte conversions.
** SVL 20220129 Fixed readLn() returning the newline separator as a part of the line.
** EVL 20220325 Base refactoring for Stream based classes getting single byte and array of bytes.
** 010 HC 20230427 Implemented server-side file-system resource. File operations performed on
** client can be optionally performed directly on server without the expensive
** remote call.
** 011 CA 20230703 The dir 'in' stream must be instance field, as multiple unnamed 'dir streams' can be
** active at a certain point.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.util;
import java.io.*;
import java.nio.*;
import java.nio.charset.*;
import com.goldencode.p2j.security.*;
import com.goldencode.util.*;
/**
* A <code>Stream</code> class implementation for the INPUT FROM OS-DIR
* Progress statement.
* <p>
* The stream will be initialized with the content of the os-dir parameter
* each line containing info in this format:
* <p>
* <b><FILE_NAME> <FILE_PATH> <FILE_TYPE> </b>
* <p>
* The file type is optional and will not be shown if <code>NO-ATTR-LIST</code>
* parameter is given to OS-DIR.
* <p>
* The implemented file type statuses are:
* <p>
* <b>PRIMARY TYPES</b>
* <ul>
* <li><b>F</b> - Regular file.
* <li><b>D</b> - Directory.
* <li><b>S</b> - Special Device
* <li><b>X</b> - Unknown type
* </ul>
* <p>
* <b>SECONDARY (ADDITIVE) TYPES</b>
* <ul>
* <li><b>H</b> - Hidden file.
* <li><b>L</b> - Symbolic link.
* <li><b>P</b> - Pipe File.
* </ul>
* <p>
* One of the main types will always appear, and can also be accompanied by
* one or more of the secondary types.
* <p>
* OsDirStream is an Input only Stream therefore some Stream operations will
* not be supported.
*/
public class DirStream
extends Stream
{
/** Error message for unsupported operation*/
private static final String UNSUPPORTED = "Not supported in OS-Dir Stream";
/** Newline separator */
public static final String NEWLINE = System.getProperty("line.separator");
/** OS file separator */
private static final String fileSep = System.getProperty("file.separator");
/** The Input stream used to store OS-Dir data */
private InputStream in = null;
/**
* Constructor for the <code>OsDirStream</code>
* <p>
* The OsDirStream will be initialized for each content of the
* given directory including itself(.) and the upper directory (..)
* with a line containing:
* <ul>
* <li>The file name.
* <li>The file path.
* <li>The file status if not specified otherwise by noAttrList.
* </ul>
*
* @param dir
* The directory name for which OS-DIR statement is applied
* @param noAttrList
* This indicates if type index will no be output to stream. If
* <code>true</code> the index will not be output to the stream,
* if <code>false</code> it will be output
* @param fsr
* The file system resource to use for this stream. May be {@code null} if no resource
* checking is required.
*/
public DirStream(String dir, boolean noAttrList, FileSystemResource fsr)
{
StringBuilder sb = new StringBuilder("");
File f = new File(dir);
try
{
if (fsr != null && !fsr.canRead(f.getCanonicalPath()))
{
String errMsg = "You do not have permission to access " + dir;
ErrorManager.recordOrThrowError(294, errMsg);
return;
}
}
catch (IOException e)
{
// ignore the IO error at this time, it will be caught later
}
// if dir doesn't exist throw error
if (!f.exists())
{
String errMsg = character.quoter(dir).getValue() + " was not found";
ErrorManager.recordOrThrowError(293, errMsg);
return;
}
// if dir is not a directory throw error
if (!f.isDirectory())
{
String errMsg = "Unable to open file: " + dir + ". Errno=2.";
ErrorManager.recordOrThrowError(98, errMsg);
return;
}
//current and upper dir are also added when we input from os-dir
//current dir is only added in windows
if (PlatformHelper.isUnderWindowsFamily())
{
File currentDir = new File(dir + fileSep + ".");
addOsDirLine(currentDir, sb, noAttrList);
sb.append(NEWLINE);
}
File upperDir = new File(dir + fileSep + "..");
addOsDirLine(upperDir, sb, noAttrList);
// get the content of directory
File[] files = f.listFiles();
if (files != null)
{
// for each content of the directory
for (File file : files)
{
sb.append(NEWLINE);
addOsDirLine(file, sb, noAttrList);
}
}
// create the input stream from the StringBuilder variable
in = new ByteArrayInputStream(sb.toString().getBytes(Charset.defaultCharset()));
}
/**
* The number of bytes available to be immediately read without blocking.
*
* @return The number of available bytes.
*
* @throws IOException
* if an I/O error occurs.
*/
public long available()
throws IOException
{
return in.available();
}
/**
* The 0-based offset into the stream at which the next read or write will
* occur. This does not work for "streams" that require sequential
* access.
*
* @return The current position in the stream.
*
* @throws UnsupportedOperationException
* If the requested operation is not supported.
* @throws IOException
* If an I/O error occurs.
*/
public long getPos()
throws UnsupportedOperationException,
IOException
{
throw new UnsupportedOperationException(UNSUPPORTED);
}
/**
* Moves the current read/write position to the specified absolute 0-based
* offset. A negative offset is ignored (no action is taken). If the
* specified offset is larger than the length of the stream, the seek
* position will report as the number requested, but if no subsequent
* writes occur to the file, the file is truncated to 0 bytes. If writes
* do occur, all writes occur at byte 0 BUT the file actually is of a size
* that is the requested offset + the number of bytes written!
* <p>
* This does not work for "streams" that require sequential access.
*
* @param pos
* The new read/write position in the stream.
*
* @throws UnsupportedOperationException
* If the requested operation is not supported.
* @throws IOException
* If an I/O error occurs.
*/
public void setPos(long pos)
throws UnsupportedOperationException,
IOException
{
throw new UnsupportedOperationException(UNSUPPORTED);
}
/**
* The length of the stream in bytes. This does not work for "streams"
* that require sequential access.
*
* @return The length of the stream.
*
* @throws UnsupportedOperationException
* If the requested operation is not supported.
* @throws IOException
* If an I/O error occurs.
*/
public long getLen()
throws UnsupportedOperationException,
IOException
{
throw new UnsupportedOperationException(UNSUPPORTED);
}
/**
* Truncates or extends the stream to the specified length if this stream
* supports such an operation. If truncation is requested, all data
* located after this point in the file is discarded. If extending the
* file is requested, the values of the data in the extended portion of
* the file is undefined.
*
* @param len
* The new length of the file.
*
* @throws UnsupportedOperationException
* If the requested operation is not supported.
* @throws IOException
* If an I/O error occurs.
*/
public void setLen(long len)
throws UnsupportedOperationException,
IOException
{
throw new UnsupportedOperationException(UNSUPPORTED);
}
/**
* Write the given character to the output stream.
*
* @param ch
* The character to be written.
*
* @throws IOException
* If an I/O error occurs.
*/
public void writeCh(char ch)
throws IOException
{
// protectWrites should handle the error
if (protectWrites())
return;
}
/**
* Write the given byte to the output stream.
*
* @param b
* The byte to be written.
*
* @throws IOException
* If an I/O error occurs.
*/
public void writeByte(byte b)
throws IOException
{
// protectWrites should handle the error
if (protectWrites())
return;
}
/**
* Write the given string to the output stream.
*
* @param data
* The data to be written.
*
* @throws IOException
* If an I/O error occurs.
*/
public void write(String data)
throws IOException
{
// protectWrites should handle the error
if (protectWrites())
{
return;
}
}
/**
* Write the given byte array to the output stream.
*
* @param data
* The data to be written.
*
* @throws IOException
* If an I/O error occurs.
*/
public void write(byte[] data)
throws IOException
{
// protectWrites should handle the error
if (protectWrites())
{
return;
}
}
/**
* Write the specified range of bytes from the given byte array to the output stream.
*
* @param data
* The data to be written.
* @param off
* Starting offset in data from which to read bytes to be written. Must be
* non-negative and {@code < data.length}.
* @param len
* Length of data to be written. Must be non-negative and {@code <= (data.length
* - offset)}.
*
* @throws IOException
* If an I/O error occurs.
*/
public void write(byte[] data, int off, int len)
throws IOException
{
// protectWrites should handle the error
if (protectWrites())
{
return;
}
}
/**
* Peeks at the character from the current read position in the stream (reads a character from
* the current read position in the stream without incrementing stream read position. The next
* {@code peekCh()} and {@code readCh()} will return the same value).
* <p>
* The underlying stream subclass determines the content of the result. Byte oriented streams
* such as pipes or files will return a byte while streams that generate keystrokes or
* characters may return a DBCS or Unicode character.
*
* @return The next character read from the stream, -1 on any failure and -2 upon an {@code EOF}.
*/
public int peekCh()
{
try
{
in.mark(2);
int ch = in.read();
in.reset();
return ch;
}
catch (EOFException e)
{
return -2; // mark as EOF
}
catch (IOException e)
{
return -1;
}
}
/**
* Read a character from the current read position in the stream.
* <p>
* The underlying stream subclass determines the content of the result.
* Byte oriented streams such as pipes or files will return a byte while
* streams that generate keystrokes or characters may return a DBCS or
* Unicode character.
*
* @return The next character read from the stream, -1 on any failure
* and -2 upon an <code>EOF</code>.
*/
public int readCh()
{
return readByte();
}
/**
* Read a single byte from the underlying stream.
*
* @return A single byte from the stream, -1 on any failure and -2 upon an {@code EOF}.
*/
@Override
public int readByte()
{
try
{
return in.read();
}
catch (EOFException e)
{
return -2; // mark as EOF
}
catch (IOException e)
{
return -1;
}
}
/**
* Read all characters from the current read position in the stream to the
* next line separator (as determined by the <code>File.separator</code>
* or to the <code>EOF</code>. Any line separator character(s) and the
* <code>EOF</code> character are not returned.
*
* @return The next line read from the stream.
*
* @throws IOException
* If an I/O error occurs.
* @throws EOFException
* If this input stream reaches the end before reading all the
* bytes.
* @throws InterruptedException
* If any thread interrupted the current thread before or while
* the current thread was waiting for a notification.
*/
public String readLn()
throws EOFException,
IOException,
InterruptedException
{
StringBuilder sb = new StringBuilder();
while (true)
{
int ch = read();
if (ch == -1)
{
// EOF
break;
}
sb.append((char) ch);
int newLineIndex = sb.indexOf(NEWLINE);
if (newLineIndex >= 0)
{
sb.delete(newLineIndex, sb.length());
break;
}
}
if (sb.length() == 0)
{
throw new EOFException();
}
return sb.toString();
}
/**
* Closes the input stream and releases OS resources associated with it.
*/
public void closeIn()
{
close();
}
/**
* This just redirects to @{link #close} since this stream type cannot be
* an output stream.
*/
public void closeOut()
{
close();
}
/**
* Close the input side of the stream and release OS resources.
*/
public void close()
{
try
{
in.close();
}
catch (IOException e)
{
// ignore
}
}
/**
* State of the input side of the stream.
*
* @return <code>true</code> if the input side of the stream is active.
*/
public boolean isIn()
{
return true;
}
/**
* State of the output side of the stream.
*
* @return <code>true</code> if the output side of the stream is active.
*/
public boolean isOut()
{
return false;
}
/**
* Assigns the internal stream reference to the given reference.
*
* @param stream
* The new internal stream reference to use for all operations.
*
* @throws UnsupportedOperationException
* If the requested operation is not supported.
*/
public void assign(Stream stream)
throws UnsupportedOperationException
{
throw new UnsupportedOperationException("Cannot handle this.");
}
/**
* Worker to read a character and hide any charset conversion that may or may not be needed.
*
* @return The next character read or -1 if EOF is reached.
*/
private int read()
{
if (true)
{
return readCh();
}
// the next code is temporarily disabled
final int MAX_BYTES = 10; // max of bytes per character in encoded text
ByteBuffer charBytes = ByteBuffer.allocate(MAX_BYTES);
for (int i = 0; i < MAX_BYTES; i++)
{
in.mark(1);
int c = readCh();
if (c < 0)
{
// even if some bytes were read, there were not enough to be able to decode a character
return c;
}
charBytes.put((byte) (c & 0x0FF));
CharBuffer chars = sourceCharset.decode(charBytes);
if (chars.position() != 1)
{
// read more than a char?
try
{
in.reset();
}
catch (IOException e)
{
// no exception expected
}
return chars.get();
}
char ch = chars.get();
if (ch != '\uFFFD') // REPLACEMENT CHARACTER ?
{
return ch;
}
}
return '\uFFFD'; // REPLACEMENT CHARACTER ?
}
/**
* Utility methods that takes a <code>File</code> input and appends to the
* given <code>StringBuilder</code> parameter:
* <ul>
* <li>The file name.
* <li>The file path.
* <li>The file status if not specified otherwise by noAttrList.
* </ul>
* <p>
*
* The file type is obtained by calling FileChecker.getFileType() which
* will execute native code to get OS specific file type.
*
* @param file
* The <code>File</code> for which the line information will be
* appended to the <code>StringBuilder variable</code>.
* @param sb
* The <code>StringBuilder</code> variable to which the information
* will be appended.
* @param noAttrList
* This indicates if type index will no be output to stream. If
* <code>true</code> the index will not be output to the stream,
* if <code>false</code> it will be output
*/
private void addOsDirLine(File file , StringBuilder sb, boolean noAttrList)
{
sb.append(character.quoter(file.getName()).toStringMessage()).append(' ')
.append(character.quoter(getPathName(file)).toStringMessage());
if (!noAttrList)
{
sb.append(' ').append(FileChecker.getFileType(getPathName(file)));
}
}
/**
* Utility method that gets the absolute path name from a <code>File</code>
* which may be given with a relative path. If current "." and upper ".."
* directory are included they will be added to the found path be added to
* the canonical path.
*
* @param file
* The <code>File</code> for which we want to return the absolute
* path.
*
* @return The absolute path of the given file for Progress format.
*/
private String getPathName(File file)
{
String pathName = "";
try
{
// Build the pathname from the canonicalPath of the parent file/directory plus filename
// because in a link file case the canonicalPath will point to the file/directory
// to which the link is pointing to.
pathName = new File(file.getParentFile().getCanonicalPath(),
file.getName()).getAbsolutePath();
}
catch (IOException e)
{
// If canonical path fails fallback on absolute path?
pathName = file.getAbsolutePath();
}
return pathName;
}
}