blob: e1ab0c838256f4a3678255f7fb869347a039f4b9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2020 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.pde.internal.ui.nls;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.wizard.IWizardContainer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.ICoreConstants;
import org.eclipse.pde.internal.core.TargetPlatformHelper;
import org.eclipse.pde.internal.core.plugin.ExternalPluginModelBase;
import org.eclipse.pde.internal.ui.PDEPlugin;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.wizards.IProjectProvider;
import org.eclipse.pde.internal.ui.wizards.plugin.FragmentFieldData;
import org.eclipse.pde.internal.ui.wizards.plugin.NewProjectCreationOperation;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
/**
*
* Generates the fragment projects for the list of selected plug-ins.
* Each fragment contains a series of properties files specific to the locales
* the user selected. For example plugin_fr.properties for the french locale
* selection. The generated locale-specific properties files contain the same
* key-value pairs as the properties file of the initial plug-in.
*
*/
public class NLSFragmentGenerator {
public static final String PLUGIN_NAME_MACRO = "${plugin_name}"; //$NON-NLS-1$
public static final String LOCALE_NAME_MACRO = "${locale}"; //$NON-NLS-1$
private static final String HTML_EXTENSION = ".html"; //$NON-NLS-1$
private static final String XML_EXTENSION = ".xml"; //$NON-NLS-1$
private static final String CLASS_EXTENSION = ".class"; //$NON-NLS-1$
private static final String JAVA_EXTENSION = ".java"; //$NON-NLS-1$
private static final String PROPERTIES_EXTENSION = ".properties"; //$NON-NLS-1$
private static final String BIN = "/bin/"; //$NON-NLS-1$
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
private static final String BACKSLASH = "\\"; //$NON-NLS-1$
private static final String SLASH = "/"; //$NON-NLS-1$
private static final String RESOURCE_FOLDER_PARENT = "/nl"; //$NON-NLS-1$
private static final double LATEST_ECLIPSE_VERSION = 3.4;
private static final String ZERO = "0"; //$NON-NLS-1$
private static final String PERIOD = "."; //$NON-NLS-1$
private static final String MIN_MINOR = ZERO;
private static final String MAX_MINOR = "9"; //$NON-NLS-1$
private static final String LEFT_SQUARE_BRACKET = "["; //$NON-NLS-1$
private static final String RIGHT_PARENTHESIS = ")"; //$NON-NLS-1$
private static final String DEFAULT_VERSION = "1.0.0"; //$NON-NLS-1$
private static final String VERSION_FORMAT_WITH_QUALIFIER = "\\d+\\.\\d+\\.\\d+\\..+"; //$NON-NLS-1$
private static final String LOCALE_INFIX_SEPERATOR = "_"; //$NON-NLS-1$
private final IWizardContainer container;
private final String template;
private final List<IPluginModelBase> plugins;
private final List<Locale> locales;
private final boolean overwriteWithoutAsking;
private IProgressMonitor monitor;
private final Filters resourceFilter = new Filters(false) {
{
add(new AbstractFilter(false) {
@Override
public boolean matches(Object object) {
String resource = object.toString();
return resource.endsWith(PROPERTIES_EXTENSION) || resource.endsWith(CLASS_EXTENSION) || resource.endsWith(JAVA_EXTENSION);
}
});
add(new AbstractFilter(false) {
@Override
public boolean matches(Object object) {
String path = object.toString();
return path.contains(BIN) || path.endsWith(SLASH) || path.endsWith(ICoreConstants.PLUGIN_FILENAME_DESCRIPTOR);
}
});
add(new AbstractFilter(true) {
@Override
public boolean matches(Object object) {
String path = object.toString();
return path.endsWith(XML_EXTENSION) || path.endsWith(HTML_EXTENSION);
}
});
}
};
private final Filters propertiesFilter = new Filters(false) {
{
add(new AbstractFilter(false) {
@Override
public boolean matches(Object object) {
String path = object.toString();
return path.contains(BIN) || path.endsWith(ICoreConstants.BUILD_FILENAME_DESCRIPTOR);
}
});
add(new AbstractFilter(true) {
@Override
public boolean matches(Object object) {
return object.toString().endsWith(PROPERTIES_EXTENSION);
}
});
}
};
public NLSFragmentGenerator(String template, List<IPluginModelBase> plugins, List<Locale> locales,
IWizardContainer container, boolean overwriteWithoutAsking) {
this.plugins = plugins;
this.locales = locales;
this.container = container;
this.template = template;
this.overwriteWithoutAsking = overwriteWithoutAsking;
}
private synchronized void setProgressMonitor(IProgressMonitor monitor) {
this.monitor = monitor;
}
private synchronized IProgressMonitor getProgressMonitor() {
return monitor;
}
public boolean generate() {
try {
final Map<String, Object> overwrites = promptForOverwrite(plugins, locales);
container.run(false, false, monitor -> {
setProgressMonitor(monitor);
try {
internationalizePlugins(plugins, locales, overwrites);
} catch (final Exception ex) {
Display.getDefault().syncExec(() -> PDEPlugin.logException(ex, ex.getMessage(),
PDEUIMessages.InternationalizeWizard_NLSFragmentGenerator_errorMessage));
}
});
} catch (Exception e) {
PDEPlugin.logException(e);
}
return true;
}
/**
* Creates an NL fragment project along with the locale specific properties
* files.
* @throws CoreException
* @throws IOException
* @throws InvocationTargetException
* @throws InterruptedException
*/
private void internationalizePlugins(List<IPluginModelBase> plugins, List<Locale> locales,
Map<String, Object> overwrites)
throws CoreException, IOException, InvocationTargetException, InterruptedException {
Set<IProject> created = new HashSet<>();
for (IPluginModelBase plugin : plugins) {
for (Locale locale : locales) {
IProject project = getNLProject(plugin, locale);
if (created.contains(project) || overwriteWithoutAsking || !project.exists() || OVERWRITE == overwrites.get(project.getName())) {
if (!created.contains(project) && project.exists()) {
project.delete(true, getProgressMonitor());
}
if (!created.contains(project)) {
createNLFragment(plugin, project, locale);
created.add(project);
project.getFolder(RESOURCE_FOLDER_PARENT).create(false, true, getProgressMonitor());
}
project.getFolder(RESOURCE_FOLDER_PARENT).getFolder(locale.toString()).create(true, true, getProgressMonitor());
createLocaleSpecificPropertiesFile(project, plugin, locale);
}
}
}
}
private Object OVERWRITE = new Object();
private Map<String, Object> promptForOverwrite(List<IPluginModelBase> plugins, List<Locale> locales) {
Map<String, Object> overwrites = new HashMap<>();
if (overwriteWithoutAsking)
return overwrites;
for (IPluginModelBase plugin : plugins) {
for (Locale locale : locales) {
IProject project = getNLProject(plugin, locale);
if (project.exists() && !overwrites.containsKey(project.getName())) {
boolean overwrite = MessageDialog.openConfirm(PDEPlugin.getActiveWorkbenchShell(), PDEUIMessages.InternationalizeWizard_NLSFragmentGenerator_overwriteTitle, NLS.bind(PDEUIMessages.InternationalizeWizard_NLSFragmentGenerator_overwriteMessage, pluginName(plugin, locale)));
overwrites.put(project.getName(), overwrite ? OVERWRITE : null);
}
}
}
return overwrites;
}
private IProject getNLProject(final IPluginModelBase name, final Locale locale) {
return ResourcesPlugin.getWorkspace().getRoot().getProject(pluginName(name, locale));
}
/**
* Creates a fragment project for the specified plug-in and populates
* the field data.
* @param plugin
* @throws CoreException
* @throws InvocationTargetException
* @throws InterruptedException
*/
private void createNLFragment(final IPluginModelBase plugin, final IProject project, final Locale locale) throws CoreException, InvocationTargetException, InterruptedException {
FragmentFieldData fragmentData = populateFieldData(plugin, locale);
IProjectProvider projectProvider = new IProjectProvider() {
@Override
public String getProjectName() {
return project.getName();
}
@Override
public IProject getProject() {
return project;
}
@Override
public IPath getLocationPath() {
return project.getLocation();
}
};
new NewProjectCreationOperation(fragmentData, projectProvider, null).run(getProgressMonitor());
}
private String pluginName(IPluginModelBase plugin, Locale locale) {
return template.replaceAll(quote(PLUGIN_NAME_MACRO), plugin.getPluginBase().getId()).replaceAll(quote(LOCALE_NAME_MACRO), locale.toString());
}
/**
* The fields are populated based on the plug-in attributes. Some fields
* are set to their default values.
*/
private FragmentFieldData populateFieldData(IPluginModelBase plugin, Locale locale) {
FragmentFieldData fragmentData = new FragmentFieldData();
fragmentData.setId(pluginName(plugin, locale));
fragmentData.setVersion(DEFAULT_VERSION);
fragmentData.setMatch(0);
fragmentData.setPluginId(plugin.getPluginBase().getId());
fragmentData.setPluginVersion(incrementRelease(plugin.getPluginBase().getVersion()));
fragmentData.setName(pluginName(plugin, locale) + " Fragment"); //$NON-NLS-1$
fragmentData.setProvider(EMPTY_STRING);
fragmentData.setSimple(false);
if (!(plugin instanceof ExternalPluginModelBase)) {
fragmentData.setSourceFolderName("src"); //$NON-NLS-1$
fragmentData.setOutputFolderName("bin"); //$NON-NLS-1$
}
fragmentData.setLegacy(false);
fragmentData.setTargetVersion(Double.toString(ensureTargetVersionCompatibility(TargetPlatformHelper.getTargetVersion())));
fragmentData.setHasBundleStructure(true);
fragmentData.setOSGiFramework(null);
fragmentData.setWorkingSets(null);
return fragmentData;
}
/**
* Adjusts the plug-in's version to reflect the required
* fragment-host bundle-version range. For example,
* fragment-host's bundle-version range would be: "[1.0.0, 1.1.0)"
* if the host's version is 1.0.0
* @param oldVersion
* @return adjusted plug-in version
*/
private String incrementRelease(String oldVersion) {
if (oldVersion.matches(VERSION_FORMAT_WITH_QUALIFIER)) {
oldVersion = oldVersion.substring(0, oldVersion.lastIndexOf(PERIOD));
}
String newVersion = LEFT_SQUARE_BRACKET + oldVersion + ',';
String oldMinor = oldVersion.substring(oldVersion.indexOf(PERIOD) + 1, oldVersion.lastIndexOf(PERIOD));
if (oldMinor.compareTo(MAX_MINOR) == 0) {
String major = Integer.toString(Integer.parseInt(oldVersion.substring(0, oldVersion.indexOf(PERIOD))) + 1);
newVersion += major + PERIOD + MIN_MINOR + PERIOD + ZERO + RIGHT_PARENTHESIS;
} else {
String major = oldVersion.substring(0, oldVersion.indexOf(PERIOD));
String newMinor = Integer.toString(Integer.parseInt(oldMinor) + 1);
newVersion += major + PERIOD + newMinor + PERIOD + ZERO + RIGHT_PARENTHESIS;
}
return newVersion;
}
/**
* Creates a locale specific properties file within the fragment project
* based on the content of the host plug-in's properties file.
* @param fragmentProject
* @param locale
* @throws CoreException
* @throws IOException
*/
private void createLocaleSpecificPropertiesFile(final IProject fragmentProject, IPluginModelBase plugin, final Locale locale) throws CoreException, IOException {
final IFolder localeResourceFolder = fragmentProject.getFolder(RESOURCE_FOLDER_PARENT).getFolder(locale.toString());
//Case 1: External plug-in
if (plugin instanceof ExternalPluginModelBase) {
final String installLocation = plugin.getInstallLocation();
//Case 1a: External plug-in is a jar file
if (new File(installLocation).isFile()) {
try (ZipFile zf = new ZipFile(installLocation)) {
for (Enumeration<? extends ZipEntry> e = zf.entries(); e.hasMoreElements();) {
worked();
ZipEntry zfe = e.nextElement();
String name = zfe.getName();
String[] segments = name.split(SLASH);
IPath path = Path.fromPortableString(join(SLASH, segments, 0, segments.length - 1));
String resourceName = segments[segments.length - 1];
String localizedResourceName = localeSpecificName(resourceName, locale);
if (propertiesFilter.include(name)) {
createParents(fragmentProject, path);
IFile file = fragmentProject.getFile(path.append(localizedResourceName));
InputStream is = zf.getInputStream(zfe);
file.create(is, false, getProgressMonitor());
} else if (resourceFilter.include(name)) {
IPath target = localeResourceFolder.getFullPath().append(path).append(resourceName);
createParents(fragmentProject, target.removeLastSegments(1).removeFirstSegments(1));
IFile file = fragmentProject.getFile(target.removeFirstSegments(1));
file.create(zf.getInputStream(zfe), false, getProgressMonitor());
}
}
}
}
//Case 1b: External plug-in has a folder structure
else {
Visitor visitor = new Visitor() {
@Override
public void visit(File file) throws CoreException, FileNotFoundException {
worked();
String relativePath = file.getAbsolutePath().substring(installLocation.length())
.replace(File.separatorChar, SLASH.charAt(0));
String[] segments = relativePath.split(SLASH);
IPath path = Path.fromPortableString(join(SLASH, segments, 0, segments.length - 1));
String resourceName = segments[segments.length - 1];
String localizedResourceName = localeSpecificName(resourceName, locale);
if (propertiesFilter.include(relativePath + (file.isDirectory() ? SLASH : EMPTY_STRING))) {
createParents(fragmentProject, path);
IFile iFile = fragmentProject.getFile(path.append(localizedResourceName));
iFile.create(new FileInputStream(file), false, getProgressMonitor());
} else if (resourceFilter.include(relativePath + (file.isDirectory() ? SLASH : EMPTY_STRING))) {
IPath target = localeResourceFolder.getFullPath().append(relativePath);
createParents(fragmentProject, target.removeLastSegments(1).removeFirstSegments(1));
IFile iFile = fragmentProject.getFile(target.removeFirstSegments(1));
iFile.create(new FileInputStream(file), false, getProgressMonitor());
}
if (file.isDirectory()) {
File[] children = file.listFiles();
for (File child : children) {
visit(child);
}
}
}
};
visitor.visit(new File(installLocation));
}
}
//Case 2: Workspace plug-in
else {
final IProject project = plugin.getUnderlyingResource().getProject();
project.accept(resource -> {
worked();
IPath parent = resource.getFullPath().removeLastSegments(1).removeFirstSegments(1);
if (propertiesFilter.include(resource)) {
String segment = localeSpecificName(resource.getFullPath().lastSegment(), locale);
IPath fragmentResource = fragmentProject.getFullPath().append(parent).append(segment);
createParents(fragmentProject, parent);
resource.copy(fragmentResource, true, getProgressMonitor());
} else if (resourceFilter.include(resource)) {
IPath target = localeResourceFolder.getFullPath().append(parent)
.append(resource.getFullPath().lastSegment());
createParents(fragmentProject, target.removeLastSegments(1).removeFirstSegments(1));
resource.copy(target, true, getProgressMonitor());
}
return true;
});
}
}
private void worked() {
Shell shell = container.getShell();
Display display = shell.getDisplay();
if (display != null && !shell.isDisposed()) {
display.readAndDispatch();
}
}
private void createParents(IProject fragmentProject, IPath parent) throws CoreException {
try {
String[] segments = parent.segments();
String path = ""; //$NON-NLS-1$
File dest = fragmentProject.getFullPath().toFile();
String destCanonicalPath = dest.getCanonicalPath();
for (String segment : segments) {
path += SLASH + segment;
IFolder folder = fragmentProject.getFolder(path);
File file = folder.getFullPath().toFile();
String canonicalFilePath = file.getCanonicalPath();
if (!canonicalFilePath.startsWith(destCanonicalPath + File.separator)) {
throw new CoreException(Status.error(
MessageFormat.format("Entry is outside of the target dir: : {0}", file.getName()), null)); //$NON-NLS-1$
}
if (!folder.exists()) {
folder.create(true, true, getProgressMonitor());
}
}
}
catch (IOException e) {
throw new CoreException(
Status.error(MessageFormat.format("IOException while processing: {0}", fragmentProject.getName()))); //$NON-NLS-1$
}
}
private String join(String delimiter, String[] parts) {
return join(delimiter, parts, 0, parts.length);
}
private String join(String delimiter, String[] parts, int offset, int n) {
StringBuilder builder = new StringBuilder();
for (int i = offset; i < n; i++) {
builder.append(parts[i]);
if (i < parts.length - 1) {
builder.append(delimiter);
}
}
return builder.toString();
}
private String localeSpecificName(String name, Locale locale) {
String[] parts = name.split(BACKSLASH + PERIOD);
parts[0] = parts[0] + LOCALE_INFIX_SEPERATOR + locale;
return join(PERIOD, parts);
}
private static class Filters {
private final List<Filter> filters = new LinkedList<>();
private final boolean default_;
public Filters(boolean default_) {
this.default_ = default_;
}
public void add(Filter filter) {
filters.add(filter);
}
public boolean include(Object object) {
if (object instanceof IResource) {
IResource resource = (IResource) object;
IPath path = IResource.FILE == resource.getType() ? resource.getFullPath() : resource.getFullPath().addTrailingSeparator();
object = path.toPortableString();
}
for (Filter filter : filters) {
if (filter.matches(object)) {
return filter.inclusive();
}
}
return default_;
}
}
private static abstract class AbstractFilter implements Filter {
private final boolean inclusive;
public AbstractFilter(boolean inclusive) {
this.inclusive = inclusive;
}
@Override
public boolean inclusive() {
return inclusive;
}
}
private static interface Filter {
boolean inclusive();
boolean matches(Object object);
}
private static interface Visitor {
void visit(File file) throws CoreException, FileNotFoundException;
}
private String quote(String pattern) {
return "\\Q" + pattern + "\\E"; //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Ensures that the target version is compatible.
* @param targetVersion
* @return target version
*/
private double ensureTargetVersionCompatibility(double targetVersion) {
if (targetVersion < 3.0) {
return LATEST_ECLIPSE_VERSION;
}
return targetVersion;
}
}