/*
* 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.service;
import org.ngrinder.common.util.PathUtils;
import org.ngrinder.common.util.ThreadUtils;
import org.ngrinder.common.util.UrlUtils;
import org.ngrinder.infra.config.Config;
import org.ngrinder.model.User;
import org.ngrinder.script.handler.ProjectHandler;
import org.ngrinder.script.handler.ScriptHandler;
import org.ngrinder.script.handler.ScriptHandlerFactory;
import org.ngrinder.script.model.FileEntry;
import org.ngrinder.script.model.FileType;
import org.ngrinder.script.repository.FileEntryRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.io.fs.FSHook;
import org.tmatesoft.svn.core.internal.io.fs.FSHookEvent;
import org.tmatesoft.svn.core.internal.io.fs.FSHooks;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNRevision;
import javax.annotation.PostConstruct;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.unmodifiableList;
import static org.ngrinder.common.util.CollectionUtils.buildMap;
import static org.ngrinder.common.util.CollectionUtils.newHashMap;
import static org.ngrinder.common.util.ExceptionUtils.processException;
import static org.ngrinder.common.util.Preconditions.checkNotEmpty;
import static org.ngrinder.common.util.Preconditions.checkNotNull;
/**
* File entry service class.
*
* This class is responsible for creating user svn repository whenever a user is
* created and connect the user to the underlying svn.
*
* @author JunHo Yoon
* @since 3.0
*/
@Service
public class FileEntryService {
private static final Logger LOG = LoggerFactory.getLogger(FileEntryService.class);
private SVNClientManager svnClientManager;
@Autowired
private Config config;
@Autowired
@Qualifier("cacheManager")
private CacheManager cacheManager;
@SuppressWarnings("SpringJavaAutowiringInspection")
@Autowired
private FileEntryRepository fileEntityRepository;
@Autowired
private ScriptHandlerFactory scriptHandlerFactory;
private Cache fileEntryCache;
/**
* Initialize {@link FileEntryService}.
*/
@PostConstruct
public void init() {
// Add cache invalidation hook.
FSHooks.registerHook(new FSHook() {
@Override
public void onHook(FSHookEvent event) throws SVNException {
if (event.getType().equals(FSHooks.SVN_REPOS_HOOK_POST_COMMIT)) {
String name = event.getReposRootDir().getName();
invalidateCache(name);
}
}
});
svnClientManager = fileEntityRepository.getSVNClientManager();
fileEntryCache = cacheManager.getCache("file_entries");
}
/**
* invalidate the file_entry_search_cache.
*
* @param userId userId.
*/
public void invalidateCache(String userId) {
fileEntryCache.evict(userId);
}
/**
* Create user svn repo.
*
* This method is executed async way.
*
* @param user newly created user.
*/
@Async
public void prepare(User user) {
File newUserDirectory = getUserRepoDirectory(user);
try {
if (!newUserDirectory.exists()) {
createUserRepo(user, newUserDirectory);
}
} catch (SVNException e) {
LOG.error("Error while prepare user {}'s repo", user.getUserName(), e);
}
}
private SVNURL createUserRepo(User user, File newUserDirectory) throws SVNException {
return svnClientManager.getAdminClient().doCreateRepository(newUserDirectory, user.getUserId(), true, true);
}
private File getUserRepoDirectory(User user) {
return new File(config.getHome().getRepoDirectoryRoot(), checkNotNull(user.getUserId()));
}
/**
* Get all {@link FileEntry} for the given user. This method is subject to
* be cached because it takes time.
*
* @param user user
* @return cached {@link FileEntry} list
*/
@Cacheable(value = "file_entries", key = "#user.userId")
public List<FileEntry> getAll(User user) {
prepare(user);
List<FileEntry> allFileEntries;
try {
allFileEntries = fileEntityRepository.findAll(user);
} catch (Exception e) {
// Try once more for the case of the underlying file system fault.
ThreadUtils.sleep(3000);
allFileEntries = fileEntityRepository.findAll(user);
}
return unmodifiableList(allFileEntries);
}
/**
* Get file entries from underlying svn for given path.
*
* @param user the user
* @param path path in the repo
* @param revision revision number. -1 if HEAD.
* @return file entry list
*/
public List<FileEntry> getAll(User user, String path, Long revision) {
// If it's not created, make one.
prepare(user);
return fileEntityRepository.findAll(user, path, revision);
}
/**
* Get file entity for the given revision.
*
* @param user the user
* @param path path in the repo
* @param revision revision. if -1, HEAD
* @return file entity
*/
public FileEntry getOne(User user, String path, Long revision) {
SVNRevision svnRev = (revision == null || revision == -1) ? SVNRevision.HEAD : SVNRevision.create(revision);
return fileEntityRepository.findOne(user, path, svnRev);
}
/**
* Get single file entity.
*
* The return value has content byte.
*
* @param user the user
* @param path path in the svn repo
* @return single file entity
*/
public FileEntry getOne(User user, String path) {
return getOne(user, path, -1L);
}
/**
* Check file existence.
*
* @param user user
* @param path path in user repo
* @return true if exists.
*/
public boolean hasFileEntry(User user, String path) {
return fileEntityRepository.hasOne(user, path);
}
/**
* Add folder on the given path.
*
* @param user user
* @param path base path
* @param comment comment
* @param folderName folder name
*/
public void addFolder(User user, String path, String folderName, String comment) {
FileEntry entry = new FileEntry();
entry.setPath(PathUtils.join(path, folderName));
entry.setFileType(FileType.DIR);
entry.setDescription(comment);
fileEntityRepository.save(user, entry, null);
}
/**
* Save File entry.
*
* @param user the user
* @param fileEntry fileEntry to be saved
*/
public void save(User user, FileEntry fileEntry) {
prepare(user);
checkNotEmpty(fileEntry.getPath());
fileEntityRepository.save(user, fileEntry, fileEntry.getEncoding());
}
/**
* Delete file entries.
*
* @param user the user
* @param basePath the base path
* @param files files under base path
*/
public void delete(User user, String basePath, String[] files) {
List<String> fullPathFiles = new ArrayList<String>();
for (String each : files) {
fullPathFiles.add(basePath + "/" + each);
}
fileEntityRepository.delete(user, fullPathFiles);
}
/**
* Delete file entry.
*
* @param user the user
* @param path the path
*/
public void delete(User user, String path) {
fileEntityRepository.delete(user, newArrayList(path));
}
String getPathFromUrl(String urlString) {
try {
URL url = new URL(urlString);
String urlPath = "/".equals(url.getPath()) ? "" : url.getPath();
return (url.getHost() + urlPath).replaceAll("[;\\&\\?\\%\\$\\-\\#]", "_");
} catch (MalformedURLException e) {
throw processException("Error while translating " + urlString, e);
}
}
String[] dividePathAndFile(String path) {
int lastIndexOf = path.lastIndexOf("/");
if (lastIndexOf == -1) {
return new String[]{"", path};
} else {
return new String[]{path.substring(0, lastIndexOf), path.substring(lastIndexOf + 1)};
}
}
/**
* Create new FileEntry.
*
* @param user user
* @param path base path path
* @param fileName fileName
* @param name name
* @param url url
* @param scriptHandler script handler
* @param libAndResource true if lib and resources should be created
* @return created file entry. main test file if it's the project creation.
*/
public FileEntry prepareNewEntry(User user, String path, String fileName, String name, String url,
ScriptHandler scriptHandler, boolean libAndResource, String options) {
if (scriptHandler instanceof ProjectHandler) {
scriptHandler.prepareScriptEnv(user, path, fileName, name, url, libAndResource,
loadTemplate(user, getScriptHandler("groovy"), url, name, options));
return null;
}
path = PathUtils.join(path, fileName);
FileEntry fileEntry = new FileEntry();
fileEntry.setPath(path);
fileEntry.setContent(loadTemplate(user, scriptHandler, url, name, options));
if (!"http://please_modify_this.com".equals(url)) {
fileEntry.setProperties(buildMap("targetHosts", UrlUtils.getHost(url)));
} else {
fileEntry.setProperties(new HashMap<String, String>());
}
return fileEntry;
}
/**
* Create new FileEntry for the given URL.
*
* @param user user
* @param url URL to be tested.
* @param scriptHandler scriptHandler
* @return created new {@link FileEntry}
*/
public FileEntry prepareNewEntryForQuickTest(User user, String url,
ScriptHandler scriptHandler) {
String path = getPathFromUrl(url);
String host = UrlUtils.getHost(url);
FileEntry quickTestFile = scriptHandler.getDefaultQuickTestFilePath(path);
String nullOptions = null;
if (scriptHandler instanceof ProjectHandler) {
String[] pathPart = dividePathAndFile(path);
prepareNewEntry(user, pathPart[0], pathPart[1], host, url, scriptHandler, false, nullOptions);
} else {
FileEntry fileEntry = prepareNewEntry(user, path, quickTestFile.getFileName(), host, url, scriptHandler,
false, nullOptions);
fileEntry.setDescription("Quick test for " + url);
save(user, fileEntry);
}
return quickTestFile;
}
/**
* Load freemarker template for quick test.
*
* @param user user
* @param handler handler
* @param url url
* @param name name
* @return generated test script
*/
public String loadTemplate(User user, ScriptHandler handler, String url, String name,
String options) {
Map<String, Object> map = newHashMap();
map.put("url", url);
map.put("userName", user.getUserName());
map.put("name", name);
map.put("options", options);
return handler.getScriptTemplate(map);
}
/**
* Get a SVN content into given dir.
*
* @param user user
* @param fromPath path in svn subPath
* @param toDir to directory
*/
public void writeContentTo(User user, String fromPath, File toDir) {
fileEntityRepository.writeContentTo(user, fromPath, toDir);
}
/**
* Get the appropriate {@link ScriptHandler} subclass for the given
* {@link FileEntry}.
*
* @param scriptEntry script entry
* @return scriptHandler
*/
public ScriptHandler getScriptHandler(FileEntry scriptEntry) {
return scriptHandlerFactory.getHandler(scriptEntry);
}
/**
* Get the appropriate {@link ScriptHandler} subclass for the given
* ScriptHandler key.
*
* @param key script entry
* @return scriptHandler
*/
public ScriptHandler getScriptHandler(String key) {
return scriptHandlerFactory.getHandler(key);
}
}