/**************************************************************************
OmegaT - Computer Assisted Translation (CAT) tool
with fuzzy matching, translation memory, keyword search,
glossaries, and translation leveraging into updated projects.
Copyright (C) 2014 Alex Buloichik
Home page: http://www.omegat.org/
Support center: http://groups.yahoo.com/group/OmegaT/
This file is part of OmegaT.
OmegaT is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OmegaT 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/
package org.omegat.core.team2;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
import org.omegat.util.FileUtil;
import org.omegat.util.StringUtil;
import gen.core.project.RepositoryDefinition;
import gen.core.project.RepositoryMapping;
/**
* Class for process some repository commands.
*
* Path, local path, repository path can be directory or one file only. Directory should be declared like
* 'source/', file should be declared like 'source/text.po'.
*
* @author Alex Buloichik (alex73mail@gmail.com)
*/
public class RemoteRepositoryProvider {
public static final String REPO_SUBDIR = ".repositories/";
public static final String REPO_PREPARE_SUBDIR = ".repositories/prep/";
final File projectRoot;
final ProjectTeamSettings teamSettings;
final List<RepositoryDefinition> repositoriesDefinitions;
final List<IRemoteRepository2> repositories = new ArrayList<IRemoteRepository2>();
public RemoteRepositoryProvider(File projectRoot, List<RepositoryDefinition> repositoriesDefinitions) throws Exception {
this.projectRoot = projectRoot;
teamSettings = new ProjectTeamSettings(new File(projectRoot, REPO_SUBDIR));
this.repositoriesDefinitions = repositoriesDefinitions;
checkDefinitions();
initializeRepositories();
}
public ProjectTeamSettings getTeamSettings() {
return teamSettings;
}
/**
* Check repository definitions in the project. TODO: define messages for user
*/
protected void checkDefinitions() {
Set<String> dirs = new TreeSet<String>();
for (RepositoryDefinition r : repositoriesDefinitions) {
if (StringUtil.isEmpty(r.getUrl())) {
throw new RuntimeException("There is no repository url");
}
if (!dirs.add(getRepositoryDir(r).getAbsolutePath())) {
throw new RuntimeException("Duplicate repository URL");
}
for (RepositoryMapping m : r.getMapping()) {
if (m.getLocal() == null || m.getRepository() == null) {
throw new RuntimeException("Wrong mapping");
}
}
}
}
/**
* Initialize repositories instances.
*/
protected void initializeRepositories() throws Exception {
for (RepositoryDefinition r : repositoriesDefinitions) {
IRemoteRepository2 repo = RemoteRepositoryFactory.create(r.getType());
repo.init(r, getRepositoryDir(r), teamSettings);
repositories.add(repo);
}
}
/**
* Find mappings for specified path.
*/
protected List<Mapping> getMappings(String path, String... forceExcludes) {
List<Mapping> result = new ArrayList<Mapping>();
for (int i = 0; i < repositoriesDefinitions.size(); i++) {
RepositoryDefinition rd = repositoriesDefinitions.get(i);
for (RepositoryMapping repoMapping : rd.getMapping()) {
Mapping m = new Mapping(path, repositories.get(i), rd, repoMapping, forceExcludes);
if (m.matches()) {
result.add(m);
}
}
}
return result;
}
/**
* Found mapping must be one.
*/
protected Mapping oneMapping(String path) {
List<Mapping> mappings = getMappings(path);
if (mappings.size() > 1) {
throw new RuntimeException("Multiple mapping for file");
} else if (mappings.isEmpty()) {
throw new RuntimeException("There is no mapping for file");
}
return mappings.get(0);
}
/**
* Checks if path is under mapping.
*/
public boolean isUnderMapping(String path) {
return !getMappings(path).isEmpty();
}
public void cleanPrepared() throws Exception {
FileUtils.deleteDirectory(new File(projectRoot, REPO_PREPARE_SUBDIR));
}
/**
* Saves file into 'prepared' dir.
*/
public File toPrepared(File inFile) throws Exception {
File dir = new File(projectRoot, REPO_PREPARE_SUBDIR);
dir.mkdirs();
File out = File.createTempFile("prepared", "", dir);
FileUtils.copyFile(inFile, out);
return out;
}
/**
* Switch all repositories into latest version.
*/
public void switchAllToLatest() throws Exception {
for (IRemoteRepository2 r : repositories) {
r.switchToVersion(null);
}
}
/**
* Switch repository that contains path to specified version. If version is null, need to switch to latest
* version.
*/
public File switchToVersion(String filePath, String version) throws Exception {
return oneMapping(filePath).switchToVersion(version);
}
/**
* Commit specific file after rebase. Used for omegat/project_save.tmx, glossaries, etc.
*/
public String commitFileAfterVersion(String path, String commentText, String... onVersions) throws Exception {
return oneMapping(path).repo.commit(onVersions, commentText);
}
/**
* Commit set of files without rebase - just local version. Used for target/*, etc.
*/
public void commitFiles(String path, String commentText) throws Exception {
Map<String, IRemoteRepository2> repos = new TreeMap<String, IRemoteRepository2>();
// collect repositories one for one mapping
for (Mapping m : getMappings(path)) {
repos.put(m.repoDefinition.getUrl(), m.repo);
}
// commit only unique repositories
for (IRemoteRepository2 repo : repos.values()) {
repo.commit(null, commentText);
}
}
/**
* Copy all mappings that under specified directory path into project directory.
*
* @param localPath
* directory name or file name
* @param forceExcludes
* exclude some path like project_save.tmx and glossary.txt
*/
public void copyFilesFromRepoToProject(String localPath, String... forceExcludes) throws Exception {
for (Mapping m : getMappings(localPath, forceExcludes)) {
m.copyFromRepoToProject();
}
}
/**
* Copy all mappings that under specified directory path into repository directory.
*
* @param localPath
* directory name or file name
* @param eolConversionCharset
* not null if EOL conversion required. EOL will be converted to repository-specific for
* existing files, and to platform-specific for new files
*/
public void copyFilesFromProjectToRepo(String localPath, String eolConversionCharset) throws Exception {
for (Mapping m : getMappings(localPath)) {
m.copyFromProjectToRepo(eolConversionCharset);
}
}
/**
* Get version of specified file.
*/
public String getVersion(String file) throws Exception {
return oneMapping(file).getVersion();
}
protected void copyFile(File from, File to, String eolConversionCharset) throws IOException {
if (eolConversionCharset != null) {
// charset defined - text file for EOL conversion
FileUtil.copyFileWithEolConversion(from, to, Charset.forName(eolConversionCharset));
} else {
// charset not defined - binary file
FileUtils.copyFile(from, to);
}
}
protected void addForCommit(IRemoteRepository2 repo, String path) throws Exception {
repo.addForCommit(path);
}
protected File getRepositoryDir(RepositoryDefinition repo) {
String path = repo.getUrl().replaceAll("[^A-Za-z0-9\\.]", "_").replaceAll("__+", "_");
return new File(new File(projectRoot, REPO_SUBDIR), path);
}
static String withLeadingSlash(String s) {
return s.startsWith("/") ? s : "/" + s;
}
static String withTrailingSlash(String s) {
return s.endsWith("/") ? s : s + "/";
}
static String withoutLeadingSlash(String s) {
return s.startsWith("/") ? s.substring(1) : s;
}
static String withoutTrailingSlash(String s) {
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
}
static String withSlashes(String s) {
return withTrailingSlash(withLeadingSlash(s));
}
static String withoutSlashes(String s) {
return withoutTrailingSlash(withoutLeadingSlash(s));
}
/**
* Class for mapping by specified local path.
*/
class Mapping {
final String filterPrefix;
final IRemoteRepository2 repo;
final RepositoryDefinition repoDefinition;
final RepositoryMapping repoMapping;
final List<String> forceExcludes;
public Mapping(String path, IRemoteRepository2 repo, RepositoryDefinition repoDefinition,
RepositoryMapping repoMapping, String... forceExcludes) {
this.repo = repo;
this.repoDefinition = repoDefinition;
this.repoMapping = repoMapping;
this.forceExcludes = new ArrayList<>();
/**
* Find common part - it should be one of path or local. If path and local have only common begin,
* they will not be mapped. I.e. path=source/ and local=source/one - it's okay, path=source/one/
* and local=source/ - also okay, but path=source/one/ and local=source/two - wrong.
*/
path = withSlashes(path);
String local = withSlashes(repoMapping.getLocal());
if (path.equals("/")) {
// root(full project path) mapping
filterPrefix = "/";
this.forceExcludes.addAll(Arrays.asList(forceExcludes));
} else if (local.equals(path)) {
// path equals mapping (path="source/" for "source/"=>"...")
filterPrefix = "/";
this.forceExcludes.addAll(getTruncatedExclusions(local, forceExcludes));
} else if (local.startsWith(path)) {
// path shorter than local and is directory (path="source/" for "source/first/..."=>"...")
filterPrefix = "/";
this.forceExcludes.addAll(getTruncatedExclusions(local, forceExcludes));
} else if (path.startsWith(local)) {
// local is shorter than path and is directory (path="omegat/project_save" for
// "omegat/"=>"...")
filterPrefix = withSlashes(path.substring(local.length()));
this.forceExcludes.addAll(Arrays.asList(forceExcludes));
} else if (local.equals("/")) {
// root(full project path) mapping (""=>"...")
filterPrefix = path;
this.forceExcludes.addAll(Arrays.asList(forceExcludes));
} else {
// otherwise path doesn't correspond with repoMapping
filterPrefix = null;
}
}
List<String> getTruncatedExclusions(String prefix, String... excludes) {
String normalizedPrefix = withSlashes(prefix);
return Stream.of(excludes).map(RemoteRepositoryProvider::withLeadingSlash)
.filter(e -> e.startsWith(normalizedPrefix))
.map(e -> withLeadingSlash(e.substring(normalizedPrefix.length())))
.collect(Collectors.toList());
}
/**
* Is path matched with mapping ?
*/
public boolean matches() {
return filterPrefix != null;
}
public void copyFromRepoToProject() throws Exception {
if (!matches()) {
throw new RuntimeException("Doesn't matched");
}
// Remove leading slashes on child args to avoid doing `new
// File("foo", "/")` which treats the "/" as an actual child element
// name and prevents proper slash normalization later on.
File from = new File(getRepositoryDir(repoDefinition), withoutLeadingSlash(repoMapping.getRepository()));
File to = new File(projectRoot, withoutLeadingSlash(repoMapping.getLocal()));
if (from.isDirectory()) {
// directory mapping
List<String> excludes = new ArrayList<>(repoMapping.getExcludes());
excludes.addAll(forceExcludes);
copy(from, to, filterPrefix, repoMapping.getIncludes(), excludes, null);
} else {
// file mapping
if (!filterPrefix.equals("/")) {
throw new RuntimeException(
"Filter prefix should have been / for file mapping, but was " + filterPrefix);
}
if (!forceExcludes.isEmpty()) {
return;
}
copyFile(from, to, null);
}
}
public void copyFromProjectToRepo(String eolConversionCharset) throws Exception {
if (!matches()) {
throw new RuntimeException("Doesn't matched");
}
// Remove leading slashes on child args to avoid doing `new
// File("foo", "/")` which treats the "/" as an actual child element
// name and prevents proper slash normalization later on.
File from = new File(projectRoot, withoutLeadingSlash(repoMapping.getLocal()));
File to = new File(getRepositoryDir(repoDefinition), withoutLeadingSlash(repoMapping.getRepository()));
if (from.isDirectory()) {
// directory mapping or full mapping
List<String> files = copy(from, to, filterPrefix, repoMapping.getIncludes(), repoMapping.getExcludes(),
eolConversionCharset);
for (String f : files) {
addForCommit(repo, withoutSlashes(f));
}
} else {
// file mapping
if (!filterPrefix.equals("/")) {
throw new RuntimeException(
"Filter prefix should have been / for file mapping, but was " + filterPrefix);
}
copyFile(from, to, eolConversionCharset);
addForCommit(repo, withoutSlashes(repoMapping.getRepository()));
}
}
public File switchToVersion(String version) throws Exception {
repo.switchToVersion(version);
File to = new File(getRepositoryDir(repoDefinition), repoMapping.getRepository());
return new File(to, filterPrefix);
}
public String getVersion() throws Exception {
return repo.getFileVersion(new File(repoMapping.getRepository(), filterPrefix).getPath());
}
/**
* @return Relative paths of copied files, <em>with <code>/</code> at
* start and end</em>
*/
protected List<String> copy(File from, File to, String prefix, List<String> includes,
List<String> excludes, String eolConversionCharset) throws Exception {
prefix = withSlashes(prefix);
List<String> relativeFiles = FileUtil.buildRelativeFilesList(from, includes, excludes);
List<String> copied = new ArrayList<String>();
for (String rf : relativeFiles) {
rf = withSlashes(rf);
if (rf.startsWith("/.repositories/")) {
continue; // list from root - shouldn't travel to .repositories/
}
if (prefix.isEmpty() || prefix.equals("/") || rf.startsWith(prefix)) {
copyFile(new File(from, rf), new File(to, rf), eolConversionCharset);
copied.add(rf);
}
}
return copied;
}
}
}