/**
* Copyright 2002-2012 the original author or authors.
*
* 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 org.springframework.integration.smb.session;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import jcifs.smb.SmbException;
import jcifs.smb.SmbFile;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.NestedIOException;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
* Implementation of the {@link Session} interface for Server Message Block (SMB)
* also known as Common Internet File System (CIFS). The Samba project set out to
* create non-Windows implementations of SMB. Often Samba is thus used synonymously to SMB.
*
* SMB is an application-layer network protocol that manages shared access to files, printers
* and other networked resources.
*
* See <a href="http://en.wikipedia.org/wiki/Server_Message_Block">Server Message Block</a>
* for more details.
*
* Inspired by the spring-integration-ftp implementation done by Mark Fisher
* and Oleg Zhurakousky.
*
* @author Markus Spann
* @author Mark Fisher
* @author Oleg Zhurakousky
*
* @since 1.0
*/
public class SmbSession implements Session<SmbFile> {
private final Log logger = LogFactory.getLog(SmbSession.class);
private static final String FILE_SEPARATOR = System.getProperty("file.separator");
private static final String SMB_FILE_SEPARATOR = "/";
static {
configureJcifs();
}
private final SmbShare smbShare;
/**
* Constructor for an SMB session.
* @param _host server NetBIOS name, DNS name, or IP address, case-insensitive
* @param _port port
* @param _domain case-sensitive domain name
* @param _user case-sensitive user name
* @param _password case-sensitive password
* @param _shareAndDir server root SMB directory
* @param _replaceFile replace existing files if true
* @param _useTempFile make use temporary files when writing
* @throws IOException in case of I/O errors
*/
SmbSession(String _host, int _port, String _domain, String _user, String _password, String _shareAndDir, boolean _replaceFile, boolean _useTempFile) throws IOException {
this(new SmbShare(new SmbConfig(_host, _port, _domain, _user, _password, _shareAndDir)));
smbShare.setReplaceFile(_replaceFile);
smbShare.setUseTempFile(_useTempFile);
}
/**
* Constructor for an SMB session.
* @param _smbShare SMB share resource
*/
public SmbSession(SmbShare _smbShare) {
Assert.notNull(_smbShare, "smbShare must not be null");
smbShare = _smbShare;
logger.debug("New " + getClass().getName() + " created.");
}
/**
* Deletes the file or directory at the specified path.
* @param _path path to a remote file or directory
* @return true if delete successful, false if resource is non-existent
* @throws IOException on error conditions returned by a CIFS server
* @see org.springframework.integration.file.remote.session.Session#remove(java.lang.String)
*/
public boolean remove(String _path) throws IOException {
Assert.hasText(_path, "path must not be empty");
boolean removed = false;
SmbFile removeFile = createSmbFileObject(_path);
if (removeFile.exists()) {
removeFile.delete();
removed = true;
}
if (!removed) {
logger.info("Could not remove non-existing resource [" + _path + "].");
} else if (logger.isInfoEnabled()) {
logger.info("Successfully removed resource [" + _path + "].");
}
return removed;
}
/**
* Returns the contents of the specified SMB resource as an array of SmbFile objects.
* In case the remote resource does not exist, an empty array is returned.
* @param _path path to a remote directory
* @return array of SmbFile objects
* @throws IOException on error conditions returned by a CIFS server or if the remote resource is not a directory.
* @see org.springframework.integration.file.remote.session.Session#list(java.lang.String)
*/
public SmbFile[] list(String _path) throws IOException {
SmbFile[] files = new SmbFile[0];
try {
SmbFile smbDir = createSmbDirectoryObject(_path);
if (!smbDir.exists()) {
logger.warn("Remote directory [" + _path + "] does not exist. Cannot list resources.");
return files;
} else if (!smbDir.isDirectory()) {
throw new NestedIOException("Resource [" + _path + "] is not a directory. Cannot list resources.");
}
files = smbDir.listFiles();
} catch (SmbException _ex) {
throw new NestedIOException("Failed to list resources in [" + _path + "].", _ex);
}
String msg = "Successfully listed " + files.length + " resource(s) in [" + _path + "]";
if (logger.isDebugEnabled()) {
logger.debug(msg + ": " + Arrays.toString(files));
} else {
logger.info(msg + ".");
}
return files;
}
/**
* Reads the remote resource specified by path and copies its contents to the specified
* {@link OutputStream}.
* @param _path path to a remote file
* @param _outputStream output stream
* @throws IOException on error conditions returned by a CIFS server or if the remote resource is not a file.
* @see org.springframework.integration.file.remote.session.Session#read(java.lang.String, java.io.OutputStream)
*/
public void read(String _path, OutputStream _outputStream) throws IOException {
Assert.hasText(_path, "path must not be empty");
Assert.notNull(_outputStream, "outputStream must not be null");
try {
SmbFile remoteFile = createSmbFileObject(_path);
if (!remoteFile.isFile()) {
throw new NestedIOException("Resource [" + _path + "] is not a file.");
}
FileCopyUtils.copy(remoteFile.getInputStream(), _outputStream);
} catch (SmbException _ex) {
throw new NestedIOException("Failed to read resource [" + _path + "].", _ex);
}
logger.info("Successfully read resource [" + _path + "].");
}
/**
* Writes contents of the specified {@link InputStream} to the remote resource
* specified by path. Remote directories are created implicitely as required.
* @param _inputStream input stream
* @param _path remote path (of a file) to write to
* @throws IOException on error conditions returned by a CIFS server
* @see org.springframework.integration.file.remote.session.Session#write(java.io.InputStream, java.lang.String)
*/
public void write(InputStream _inputStream, String _path) throws IOException {
Assert.notNull(_inputStream, "inputStream must not be empty");
Assert.hasText(_path, "path must not be null");
try {
mkdirs(_path);
SmbFile targetFile = createSmbFileObject(_path);
if (smbShare.isUseTempFile()) {
String tempFileName = _path + smbShare.newTempFileSuffix();
SmbFile tempFile = createSmbFileObject(tempFileName);
tempFile.createNewFile();
Assert.isTrue(tempFile.canWrite(), "Temporary file [" + tempFileName + "] is not writable.");
FileCopyUtils.copy(_inputStream, tempFile.getOutputStream());
if (targetFile.exists() && smbShare.isReplaceFile()) {
targetFile.delete();
}
tempFile.renameTo(targetFile);
} else {
FileCopyUtils.copy(_inputStream, targetFile.getOutputStream());
}
} catch (SmbException _ex) {
throw new NestedIOException("Failed to write resource [" + _path + "].", _ex);
}
logger.info("Successfully wrote remote file [" + _path + "].");
}
/**
* Convenience method to write a local file object to a remote location.
* @see org.springframework.integration.smb.session.SmbSession#write(InputStream, String)
*/
public SmbFile write(File _file, String _path) throws IOException {
return writeAndClose(new FileInputStream(_file), _path);
}
/**
* Convenience method to write a byte array to a remote location.
* @see org.springframework.integration.smb.session.SmbSession#write(InputStream, String)
*/
public SmbFile write(byte[] _contents, String _path) throws IOException {
return writeAndClose(new ByteArrayInputStream(_contents), _path);
}
/**
* Creates the specified remote path if not yet exists.
* If the specified resource is a file rather than a path, creates all directories leading
* to that file.
* @param _path remote path to create
* @return always true (error states are express by exceptions)
* @throws IOException on error conditions returned by a CIFS server
* @see org.springframework.integration.file.remote.session.Session#mkdir(java.lang.String)
*/
public boolean mkdir(String _path) throws IOException {
try {
SmbFile dir = createSmbDirectoryObject(_path);
if (!dir.exists()) {
dir.mkdirs();
logger.info("Successfully created remote directory [" + _path + "] in share [" + smbShare + "].");
} else {
logger.info("Remote directory [" + _path + "] exists in share [" + smbShare + "].");
}
return true;
} catch (SmbException _ex) {
throw new NestedIOException("Failed to create directory [" + _path + "].", _ex);
}
}
/**
* Checks whether the remote resource exists.
* @param _path remote path
* @return true if exists, false otherwise
* @throws IOException on error conditions returned by a CIFS server
* @see org.springframework.integration.file.remote.session.Session#exists(java.lang.String)
*/
public boolean exists(String _path) throws IOException {
return createSmbFileObject(_path).exists();
}
/**
* Checks whether the remote resource is a file.
* @param _path remote path
* @return true if resource is a file, false otherwise
* @throws IOException on error conditions returned by a CIFS server
*/
public boolean isFile(String _path) throws IOException {
SmbFile resource = createSmbFileObject(_path);
return resource.exists() && resource.isFile();
}
/**
* Checks whether the remote resource is a directory.
* @param _path remote path
* @return true if resource is a directory, false otherwise
* @throws IOException on error conditions returned by a CIFS server
*/
public boolean isDirectory(String _path) throws IOException {
SmbFile resource = createSmbFileObject(_path);
return resource.exists() && resource.isDirectory();
}
/**
* Create all directories in the given remote path reference.
* @param _path path remote path, may be a file in which case the file name is ignored
* @return the path created or null
* @throws IOException on error conditions returned by a CIFS server
*/
String mkdirs(String _path) throws IOException {
int idxPath = _path.lastIndexOf(FILE_SEPARATOR);
if (idxPath > -1) {
String path = _path.substring(0, idxPath + 1);
mkdir(path);
return path;
}
return null;
}
/**
* Renames a remote resource.
* @param _pathFrom remote source path
* @param _pathTo remote target path
* @throws IOException on error conditions returned by a CIFS server
* @see org.springframework.integration.file.remote.session.Session#rename(java.lang.String, java.lang.String)
*/
public void rename(String _pathFrom, String _pathTo) throws IOException {
try {
SmbFile smbFileFrom = createSmbFileObject(_pathFrom);
SmbFile smbFileTo = createSmbFileObject(_pathTo);
if (smbShare.isReplaceFile() && smbFileTo.exists()) {
smbFileTo.delete();
}
smbFileFrom.renameTo(smbFileTo);
} catch (SmbException _ex) {
throw new NestedIOException("Failed to rename [" + _pathFrom + "] to [" + _pathTo + "].", _ex);
}
logger.info("Successfully renamed remote resource [" + _pathFrom + "] to [" + _pathTo + "].");
}
/**
* Closes this SMB session.
* @see org.springframework.integration.file.remote.session.Session#close()
*/
public void close() {
smbShare.doClose();
}
/**
* Checks with this SMB session is open and ready for work by attempting
* to list remote files and checking for error conditions..
* @return true if the session is open, false otherwise
* @see org.springframework.integration.file.remote.session.Session#isOpen()
*/
public boolean isOpen() {
if (!smbShare.isOpened()) {
return false;
}
try {
smbShare.listFiles();
} catch (Exception _ex) {
close();
}
return smbShare.isOpened();
}
/**
* Convenience method to write the specified input stream to a remote path and return
* the path as an SMB file object.
* @param _inputStream input stream
* @param _path remote path (of a file) to write to
* @return SMB file object
* @throws IOException on error conditions returned by a CIFS server
*/
SmbFile writeAndClose(InputStream _inputStream, String _path) throws IOException {
write(_inputStream, _path);
_inputStream.close();
return createSmbFileObject(_path);
}
/**
* Factory method for new SmbFile objects under this session's share for the specified path.
* @param path remote path
* @param isDirectory Boolean object to indicate the path is a directory, may be null
* @return SmbFile object for path
* @throws IOException in case of I/O errors
*/
private SmbFile createSmbFileObject(String path, Boolean isDirectory) throws IOException {
final String cleanedPath = StringUtils.cleanPath(path);
if (!StringUtils.hasText(cleanedPath)) {
return smbShare;
}
SmbFile smbFile = new SmbFile(smbShare, cleanedPath);
boolean appendFileSeparator = !cleanedPath.endsWith(SMB_FILE_SEPARATOR);
if (appendFileSeparator) {
try {
appendFileSeparator = smbFile.isDirectory() || (isDirectory != null && isDirectory);
} catch (SmbException ex) {
appendFileSeparator = false;
}
}
if (appendFileSeparator) {
smbFile = createSmbFileObject(cleanedPath + SMB_FILE_SEPARATOR);
}
if (logger.isDebugEnabled()) {
logger.debug("Created new " + SmbFile.class.getName() + "[" + smbFile + "] for path [" + path + "].");
}
return smbFile;
}
/**
* Creates an SMB file object pointing to a remote file.
*/
public SmbFile createSmbFileObject(String _path) throws IOException {
return createSmbFileObject(_path, null);
}
/**
* Creates an SMB file object pointing to a remote directory.
*/
public SmbFile createSmbDirectoryObject(String _path) throws IOException {
return createSmbFileObject(_path, true);
}
/**
* Static configuration of the JCIFS library.
* The log level of this class is mapped to a suitable <code>jcifs.util.loglevel</code>
*/
static void configureJcifs() {
// TODO jcifs.Config.setProperty("jcifs.smb.client.useExtendedSecurity", "false");
// TODO jcifs.Config.setProperty("jcifs.smb.client.disablePlainTextPasswords", "false");
// set JCIFS SMB client library' log level unless already configured by system property
final String sysPropLogLevel = "jcifs.util.loglevel";
if (jcifs.Config.getProperty(sysPropLogLevel) == null) {
// set log level according to this class' logger's log level.
Log log = LogFactory.getLog(SmbSession.class);
if (log.isTraceEnabled()) {
jcifs.Config.setProperty(sysPropLogLevel, "N");
} else if (log.isDebugEnabled()) {
jcifs.Config.setProperty(sysPropLogLevel, "3");
} else {
jcifs.Config.setProperty(sysPropLogLevel, "1");
}
}
}
@Override
public String[] listNames(String path) throws IOException {
throw new UnsupportedOperationException("Not implemented yet");
}
}