/*
* 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.ngrinder.script.repository;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang.StringUtils;
import org.ngrinder.common.exception.NGrinderRuntimeException;
import org.ngrinder.common.model.Home;
import org.ngrinder.common.util.EncodingUtils;
import org.ngrinder.infra.config.Config;
import org.ngrinder.model.User;
import org.ngrinder.script.model.FileCategory;
import org.ngrinder.script.model.FileEntry;
import org.ngrinder.script.model.FileType;
import org.ngrinder.user.repository.UserRepository;
import org.ngrinder.user.service.UserContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.EmptyStackException;
import java.util.List;
import java.util.Map.Entry;
import static org.ngrinder.common.util.CollectionUtils.newArrayList;
import static org.ngrinder.common.util.ExceptionUtils.processException;
import static org.ngrinder.common.util.NoOp.noOp;
import static org.ngrinder.common.util.Preconditions.checkNotNull;
/**
* SVN FileEntity repository.
*
* This class save and retrieve {@link FileEntry} from Local SVN folders.
*
* @author JunHo Yoon
* @since 3.0
*/
@Profile("production")
@Component
public class FileEntryRepository {
private static final Logger LOG = LoggerFactory.getLogger(FileEntryRepository.class);
@Autowired
private Config config;
private Home home;
private File subversionHome;
/**
* Initialize the {@link FileEntryRepository}. This method should be
* performed to set up FS Repository.
*/
@PostConstruct
public void init() {
FSRepositoryFactory.setup();
home = config.getHome();
subversionHome = home.getSubFile("subversion");
}
@Autowired
private UserRepository userRepository;
/**
* Get user repository.
*
* For unit test, This can be overridable.
*
* @param user the user
* @return user repository path.
*/
public File getUserRepoDirectory(User user) {
return home.getUserRepoDirectory(user.getUserId());
}
/**
* Return all {@link FileEntry}s under the given path.
*
* @param user user
* @param path path under which files are searched.
* @param revision . null if head.
* @return found {@link FileEntry}s
*/
public List<FileEntry> findAll(User user, final String path, Long revision) {
return findAll(user, path, revision, false);
}
/**
* Return all {@link FileEntry}s under the given path.
*
* @param user user
* @param path path under which files are searched.
* @param revision null if head.
* @param recursive true if recursive finding
* @return found {@link FileEntry}s
*/
public List<FileEntry> findAll(User user, final String path, Long revision, boolean recursive) {
SVNRevision svnRevision = SVNRevision.HEAD;
if (revision != null && -1L != revision) {
svnRevision = SVNRevision.create(revision);
}
final List<FileEntry> fileEntries = newArrayList();
SVNClientManager svnClientManager = getSVNClientManager();
try {
svnClientManager.getLogClient().doList(SVNURL.fromFile(getUserRepoDirectory(user)).appendPath(path, true),
svnRevision, svnRevision, true, recursive, new ISVNDirEntryHandler() {
@Override
public void handleDirEntry(SVNDirEntry dirEntry) throws SVNException {
FileEntry script = new FileEntry();
// Exclude base path "/"
if (StringUtils.isBlank(dirEntry.getRelativePath())) {
return;
}
script.setPath(FilenameUtils.normalize(path + "/" + dirEntry.getRelativePath(), true));
script.setCreatedDate(dirEntry.getDate());
script.setLastModifiedDate(dirEntry.getDate());
script.setDescription(dirEntry.getCommitMessage());
script.setRevision(dirEntry.getRevision());
if (dirEntry.getKind() == SVNNodeKind.DIR) {
script.setFileType(FileType.DIR);
} else {
script.getFileType();
script.setFileSize(dirEntry.getSize());
}
fileEntries.add(script);
}
});
} catch (Exception e) {
LOG.debug("findAll() to the not existing folder {}", path);
} finally {
closeSVNClientManagerQuietly(svnClientManager);
}
return fileEntries;
}
/**
* Return all {@link FileEntry}s which user have. It excludes
* {@link FileType#DIR} entries.
*
* @param user user
* @return found {@link FileEntry}s
*/
public List<FileEntry> findAll(final User user) {
final List<FileEntry> scripts = newArrayList();
SVNClientManager svnClientManager = getSVNClientManager();
try {
svnClientManager.getLogClient().doList(SVNURL.fromFile(getUserRepoDirectory(user)), SVNRevision.HEAD,
SVNRevision.HEAD, false, true, new ISVNDirEntryHandler() {
@Override
public void handleDirEntry(SVNDirEntry dirEntry) throws SVNException {
FileEntry script = new FileEntry();
String relativePath = dirEntry.getRelativePath();
if (StringUtils.isBlank(relativePath)) {
return;
}
script.setCreatedDate(dirEntry.getDate());
script.setLastModifiedDate(dirEntry.getDate());
script.setPath(relativePath);
script.setDescription(dirEntry.getCommitMessage());
long reversion = dirEntry.getRevision();
script.setRevision(reversion);
script.setFileType(dirEntry.getKind() == SVNNodeKind.DIR ? FileType.DIR : null);
script.setFileSize(dirEntry.getSize());
scripts.add(script);
}
});
} catch (Exception e) {
LOG.error("Error while fetching files from SVN for {}", user.getUserId());
LOG.debug("Error details :", e);
throw new NGrinderRuntimeException(e);
} finally {
closeSVNClientManagerQuietly(svnClientManager);
}
return scripts;
}
/**
* Return a {@link FileEntry} for the given path and revision.
*
* @param user user
* @param path path in the svn repo
* @param revision revision of the file
* @return found {@link FileEntry}, null if not found
*/
public FileEntry findOne(User user, String path, SVNRevision revision) {
final FileEntry script = new FileEntry();
SVNClientManager svnClientManager = null;
ByteArrayOutputStream outputStream = null;
try {
svnClientManager = getSVNClientManager();
SVNURL userRepoUrl = SVNURL.fromFile(getUserRepoDirectory(user));
if (userRepoUrl == null) {
return null;
}
SVNRepository repo = svnClientManager.createRepository(userRepoUrl, true);
SVNNodeKind nodeKind = repo.checkPath(path, -1);
if (nodeKind == SVNNodeKind.NONE) {
return null;
}
outputStream = new ByteArrayOutputStream();
SVNProperties fileProperty = new SVNProperties();
// Get File.
repo.getFile(path, revision.getNumber(), fileProperty, outputStream);
SVNDirEntry lastRevisionedEntry = repo.info(path, -1);
long lastRevisionNumber = (lastRevisionedEntry == null) ? -1 : lastRevisionedEntry.getRevision();
String revisionStr = fileProperty.getStringValue(SVNProperty.REVISION);
long revisionNumber = Long.parseLong(revisionStr);
SVNDirEntry info = repo.info(path, revisionNumber);
byte[] byteArray = outputStream.toByteArray();
script.setPath(path);
for (String name : fileProperty.nameSet()) {
script.getProperties().put(name, fileProperty.getStringValue(name));
}
script.setFileType(FileType.getFileTypeByExtension(FilenameUtils.getExtension(script.getFileName())));
if (script.getFileType().isEditable()) {
String autoDetectedEncoding = EncodingUtils.detectEncoding(byteArray, "UTF-8");
script.setContent(new String(byteArray, autoDetectedEncoding));
script.setEncoding(autoDetectedEncoding);
script.setContentBytes(byteArray);
} else {
script.setContentBytes(byteArray);
}
script.setDescription(info.getCommitMessage());
script.setRevision(revisionNumber);
script.setLastRevision(lastRevisionNumber);
script.setCreatedUser(user);
} catch (Exception e) {
LOG.error("Error while fetching a file from SVN {}", user.getUserId() + "_" + path, e);
return null;
} finally {
closeSVNClientManagerQuietly(svnClientManager);
IOUtils.closeQuietly(outputStream);
}
return script;
}
private void addPropertyValue(ISVNEditor editor, FileEntry fileEntry) throws SVNException {
if (fileEntry.getFileType().getFileCategory() == FileCategory.SCRIPT) {
editor.changeFileProperty(fileEntry.getPath(), "targetHosts", SVNPropertyValue.create(""));
}
for (Entry<String, String> each : fileEntry.getProperties().entrySet()) {
editor.changeFileProperty(fileEntry.getPath(), each.getKey(), SVNPropertyValue.create(each.getValue()));
}
}
/**
* Save fileEntry on the {@link FileEntry.getPath()} location.
*
* @param user the user
* @param fileEntry fileEntry to be saved
* @param encoding file encoding with which fileEntry is saved. It is meaningful
* only FileEntry is editable.
*/
public void save(User user, FileEntry fileEntry, String encoding) {
SVNClientManager svnClientManager = null;
ISVNEditor editor = null;
String checksum = null;
InputStream bais = null;
try {
svnClientManager = getSVNClientManager();
SVNRepository repo = svnClientManager.createRepository(SVNURL.fromFile(getUserRepoDirectory(user)), true);
SVNDirEntry dirEntry = repo.info(fileEntry.getPath(), -1);
// Add base paths
String fullPath = "";
// Check.. first
for (String each : getPathFragment(fileEntry.getPath())) {
fullPath = fullPath + "/" + each;
SVNDirEntry folderStepEntry = repo.info(fullPath, -1);
if (folderStepEntry != null && folderStepEntry.getKind() == SVNNodeKind.FILE) {
throw processException("User " + user.getUserId() + " tried to create folder "
+ fullPath + ". It's file..");
}
}
editor = repo.getCommitEditor(fileEntry.getDescription(), null, true, null);
editor.openRoot(-1);
fullPath = "";
for (String each : getPathFragment(fileEntry.getPath())) {
fullPath = fullPath + "/" + each;
try {
editor.addDir(fullPath, null, -1);
} catch (Exception e) {
// FALL THROUGH
noOp();
}
}
if (fileEntry.getFileType() == FileType.DIR) {
editor.addDir(fileEntry.getPath(), null, -1);
} else {
if (dirEntry == null) {
// If it's new file
editor.addFile(fileEntry.getPath(), null, -1);
} else {
// If it's modification
editor.openFile(fileEntry.getPath(), -1);
}
editor.applyTextDelta(fileEntry.getPath(), null);
// Calc diff
final SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
if (fileEntry.getContentBytes() == null && fileEntry.getFileType().isEditable()) {
bais = new ByteArrayInputStream(checkNotNull(fileEntry.getContent()).getBytes(
encoding == null ? "UTF-8" : encoding));
} else {
bais = new ByteArrayInputStream(fileEntry.getContentBytes());
}
checksum = deltaGenerator.sendDelta(fileEntry.getPath(), bais, editor, true);
}
addPropertyValue(editor, fileEntry);
editor.closeFile(fileEntry.getPath(), checksum);
} catch (Exception e) {
abortSVNEditorQuietly(editor);
// If it's adding the folder which already exists... ignore..
if (e instanceof SVNException && fileEntry.getFileType() == FileType.DIR) {
if (SVNErrorCode.FS_ALREADY_EXISTS.equals(((SVNException) e).getErrorMessage().getErrorCode())) {
return;
}
}
LOG.error("Error while saving file to SVN", e);
throw processException("Error while saving file to SVN", e);
} finally {
closeSVNEditorQuietly(editor);
closeSVNClientManagerQuietly(svnClientManager);
IOUtils.closeQuietly(bais);
}
}
String[] getPathFragment(String path) {
String basePath = FilenameUtils.getPath(path);
return StringUtils.split(FilenameUtils.separatorsToUnix(basePath), "/");
}
/**
* Quietly close svn editor.
*
* @param editor editor to be closed.
*/
private void abortSVNEditorQuietly(ISVNEditor editor) {
if (editor == null) {
return;
}
try {
editor.abortEdit();
} catch (SVNException e) {
// FALL THROUGH
noOp();
}
}
/**
* Quietly close svn editor. This is convenient method.
*
* @param editor editor to be closed.
*/
private void closeSVNEditorQuietly(ISVNEditor editor) {
if (editor == null) {
return;
}
try {
// recursively close
//noinspection InfiniteLoopStatement
while (true) {
editor.closeDir();
}
} catch (EmptyStackException e) {
// FALL THROUGH
noOp();
} catch (SVNException e) {
// FALL THROUGH
noOp();
} finally {
try {
editor.closeEdit();
} catch (SVNException e) {
// FALL THROUGH
noOp();
}
}
}
/**
* Delete file entries on given paths. If the one of paths does not exist,
* all deletion is canceled.
*
* @param user user
* @param paths paths of file entries.
*/
public void delete(User user, List<String> paths) {
SVNClientManager svnClientManager = null;
ISVNEditor editor = null;
try {
svnClientManager = getSVNClientManager();
SVNRepository repo = svnClientManager.createRepository(SVNURL.fromFile(getUserRepoDirectory(user)), true);
editor = repo.getCommitEditor("delete", null, true, null);
editor.openRoot(-1);
for (String each : paths) {
editor.deleteEntry(each, -1);
}
} catch (Exception e) {
abortSVNEditorQuietly(editor);
LOG.error("Error while deleting file from SVN", e);
throw processException("Error while deleting files from SVN", e);
} finally {
closeSVNEditorQuietly(editor);
closeSVNClientManagerQuietly(svnClientManager);
}
}
@Autowired
UserContext userContext;
/**
* Get svn client manager with the designated subversionHome.
*
* @return svn client manager
*/
public SVNClientManager getSVNClientManager() {
DefaultSVNOptions options = SVNWCUtil.createDefaultOptions(subversionHome, true);
ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(subversionHome,
getCurrentUserId(), null, false);
return SVNClientManager.newInstance(options, authManager);
}
protected String getCurrentUserId() {
try {
return userContext.getCurrentUser().getUserId();
} catch (Exception e) {
return "default";
}
}
private void closeSVNClientManagerQuietly(SVNClientManager svnClientManager) {
if (svnClientManager != null) {
svnClientManager.dispose();
}
}
/**
* Check file existence.
*
* @param user user
* @param path path in user repo
* @return true if exists.
*/
public boolean hasOne(User user, String path) {
SVNClientManager svnClientManager = null;
try {
svnClientManager = getSVNClientManager();
SVNURL userRepoUrl = SVNURL.fromFile(getUserRepoDirectory(user));
SVNRepository repo = svnClientManager.createRepository(userRepoUrl, true);
SVNNodeKind nodeKind = repo.checkPath(path, -1);
return (nodeKind != SVNNodeKind.NONE);
} catch (Exception e) {
LOG.error("Error while fetching files from SVN", e);
throw processException("Error while checking file existence from SVN", e);
} finally {
closeSVNClientManagerQuietly(svnClientManager);
}
}
/**
* Copy {@link FileEntry} to the given path.
*
* This method only work for the file not dir.
*
* @param user user
* @param path path of {@link FileEntry}
* @param toPathDir file dir path to write.
*/
public void writeContentTo(User user, String path, File toPathDir) {
SVNClientManager svnClientManager = null;
FileOutputStream fileOutputStream = null;
try {
svnClientManager = getSVNClientManager();
SVNURL userRepoUrl = SVNURL.fromFile(getUserRepoDirectory(user));
SVNRepository repo = svnClientManager.createRepository(userRepoUrl, true);
SVNNodeKind nodeKind = repo.checkPath(path, -1);
// If it's DIR, it does not work.
if (nodeKind == SVNNodeKind.NONE || nodeKind == SVNNodeKind.DIR) {
throw processException("It's not possible to write directory. nodeKind is " + nodeKind);
}
//noinspection ResultOfMethodCallIgnored
toPathDir.mkdirs();
File destFile = new File(toPathDir, FilenameUtils.getName(path));
// Prepare parent folders
fileOutputStream = new FileOutputStream(destFile);
SVNProperties fileProperty = new SVNProperties();
// Get file.
repo.getFile(path, -1L, fileProperty, fileOutputStream);
} catch (Exception e) {
LOG.error("Error while fetching files from SVN", e);
throw processException("Error while fetching files from SVN", e);
} finally {
closeSVNClientManagerQuietly(svnClientManager);
IOUtils.closeQuietly(fileOutputStream);
}
}
}