blob: 4412f986766d80249389452424279c810b49db17 [file] [log] [blame]
/*
* Copyright (c) 2013 Eike Stepper (Berlin, Germany) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eike Stepper - initial API and implementation
*/
package org.eclipse.emf.cdo.releng.gitbash.repository;
import org.eclipse.emf.cdo.releng.gitbash.AbstractAction;
import org.eclipse.emf.cdo.releng.gitbash.Activator;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Eike Stepper
*/
public class UpdateCopyrightsAction extends AbstractAction<Repository>
{
private static final String[] IGNORED_PATHS = { "resourcemanager.java", "menucardtemplate.java",
"org.eclipse.emf.cdo.releng.version.tests/tests", "org.eclipse.net4j.jms.api/src",
"org/eclipse/net4j/util/ui/proposals/", "org.eclipse.emf.cdo.examples.installer/examples",
"org.eclipse.net4j.examples.installer/examples" };
private static final String[] REQUIRED_EXTENSIONS = { ".java", ".ant", "build.xml", "plugin.xml", "fragment.xml",
"feature.xml", "plugin.properties", "fragment.properties", "feature.properties", "about.properties",
"build.properties", "messages.properties", "copyright.txt", ".exsd", "org.eclipse.jdt.ui.prefs" };
private static final String[] OPTIONAL_EXTENSIONS = { ".properties", ".xml", ".css", ".ecore", ".genmodel", ".mwe",
".xpt", ".ext" };
private static final String[] IGNORED_MESSAGE_VERBS = { "update", "adjust", "fix" };
private static final String[] IGNORED_MESSAGE_NOUNS = { "copyright", "legal header" };
private static final String[] IGNORED_MESSAGES = combineWords(IGNORED_MESSAGE_VERBS, IGNORED_MESSAGE_NOUNS);
private static final Pattern COPYRIGHT_PATTERN = Pattern
.compile("(.*?)Copyright \\(c\\) ([0-9 ,-]+) (.*?) and others\\.(.*)");
private static final Calendar CALENDAR = GregorianCalendar.getInstance();
private static final int CURRENT_YEAR = CALENDAR.get(Calendar.YEAR);
private static final String CURRENT_YEAR_STRING = Integer.toString(CURRENT_YEAR);
private static final IWorkbench WORKBENCH = PlatformUI.getWorkbench();
private static final String NL = System.getProperty("line.separator");
private Git git;
private File workTree;
private int workTreeLength;
private List<String> missingCopyrights = new ArrayList<String>();
private int rewriteCount;
private int fileCount;
public UpdateCopyrightsAction()
{
super(Repository.class);
}
protected boolean isCheckOnly()
{
return false;
}
@Override
protected void run(final Shell shell, final Repository repository) throws Exception
{
IRunnableWithProgress runnable = new IRunnableWithProgress()
{
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException
{
try
{
git = new Git(repository);
workTree = repository.getWorkTree();
workTreeLength = workTree.getAbsolutePath().length() + 1;
fileCount = countFiles(workTree);
monitor.beginTask(getTitle(), fileCount);
final long start = System.currentTimeMillis();
checkFolder(monitor, workTree);
shell.getDisplay().syncExec(new Runnable()
{
public void run()
{
try
{
handleResult(shell, workTree, formatDuration(start));
}
catch (Exception ex)
{
Activator.log(ex);
}
}
});
}
catch (OperationCanceledException ex)
{
// Do nothing
}
catch (Exception ex)
{
throw new InvocationTargetException(ex);
}
finally
{
missingCopyrights.clear();
rewriteCount = 0;
fileCount = 0;
workTreeLength = 0;
workTree = null;
git = null;
monitor.done();
}
}
};
WORKBENCH.getProgressService().run(true, true, runnable);
}
private int countFiles(File folder) throws Exception
{
int count = 0;
for (File file : folder.listFiles())
{
String fileName = file.getName();
if (file.isDirectory() && !fileName.equals(".git"))
{
count += countFiles(file);
}
else
{
String path = getPath(file);
if (hasString(path, IGNORED_PATHS))
{
continue;
}
++count;
}
}
return count;
}
private void checkFolder(IProgressMonitor monitor, File folder) throws Exception
{
for (File file : folder.listFiles())
{
if (monitor.isCanceled())
{
throw new OperationCanceledException();
}
String fileName = file.getName();
if (file.isDirectory() && !fileName.equals(".git"))
{
checkFolder(monitor, file);
}
else
{
if (checkFile(monitor, file))
{
monitor.worked(1);
}
}
}
}
private boolean checkFile(IProgressMonitor monitor, File file) throws Exception
{
String path = getPath(file);
if (!hasString(path, IGNORED_PATHS))
{
boolean required = hasExtension(file, REQUIRED_EXTENSIONS);
boolean optional = required || hasExtension(file, OPTIONAL_EXTENSIONS);
boolean checkOnly = isCheckOnly();
if (checkOnly ? required : optional)
{
monitor.subTask(path);
List<String> lines = new ArrayList<String>();
boolean copyrightFound = false;
boolean copyrightChanged = false;
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);
try
{
String line;
while ((line = bufferedReader.readLine()) != null)
{
Matcher matcher = COPYRIGHT_PATTERN.matcher(line);
if (matcher.matches())
{
copyrightFound = true;
if (checkOnly)
{
break;
}
String prefix = matcher.group(1);
String dates = matcher.group(2);
String owner = matcher.group(3);
String suffix = matcher.group(4);
if (dates.endsWith(CURRENT_YEAR_STRING))
{
// This file must have been rewritten already in the current year
break;
}
String newLine = rewriteCopyright(path, line, prefix, dates, owner, suffix);
if (newLine != line)
{
line = newLine;
copyrightChanged = true;
}
}
lines.add(line);
}
}
finally
{
close(bufferedReader);
close(fileReader);
}
if (required && !copyrightFound)
{
missingCopyrights.add(path);
}
if (copyrightChanged)
{
writeLines(file, lines);
++rewriteCount;
}
}
return true;
}
return false;
}
private String rewriteCopyright(String path, String line, String prefix, String dates, String owner, String suffix)
throws Exception
{
String newDates;
if (path.endsWith("org.eclipse.jdt.ui.prefs") || path.endsWith("copyright.txt")
|| path.endsWith("org.eclipse.emf.cdo.license-feature/feature.properties")
|| suffix.equals(" All rights reserved.\\n\\"))
{
// Special handling of occurrences with a more global (then file-scoped) meaning.
// Special handling of second occurrence in about.properties (which is visible in the UI).
newDates = "2004-" + CURRENT_YEAR;
}
else
{
Set<Integer> years = new HashSet<Integer>();
for (RevCommit commit : git.log().addPath(path).call())
{
String message = commit.getFullMessage();
if (!hasString(message, IGNORED_MESSAGES))
{
CALENDAR.setTimeInMillis(1000L * commit.getCommitTime());
int year = CALENDAR.get(Calendar.YEAR);
years.add(year);
}
}
if (years.isEmpty())
{
// The file doesn't have a history, may be derived.
return line;
}
newDates = formatYears(years);
}
if (newDates.equals(dates))
{
return line;
}
return prefix + "Copyright (c) " + newDates + " " + owner + " and" + " others." + suffix;
}
private String formatYears(Collection<Integer> years)
{
class YearRange
{
private int begin;
private int end;
public YearRange(int begin)
{
this.begin = begin;
end = begin;
}
public boolean add(int year)
{
if (year == end + 1)
{
end = year;
return true;
}
return false;
}
@Override
public String toString()
{
if (begin == end)
{
return "" + begin;
}
if (begin == end - 1)
{
return "" + begin + ", " + end;
}
return "" + begin + "-" + end;
}
}
List<Integer> list = new ArrayList<Integer>(years);
Collections.sort(list);
List<YearRange> ranges = new ArrayList<YearRange>();
YearRange lastRange = null;
for (Integer year : list)
{
if (lastRange == null || !lastRange.add(year))
{
lastRange = new YearRange(year);
ranges.add(lastRange);
}
}
StringBuilder builder = new StringBuilder();
for (YearRange range : ranges)
{
if (builder.length() != 0)
{
builder.append(", ");
}
builder.append(range);
}
return builder.toString();
}
private String getTitle()
{
return (isCheckOnly() ? "Check" : "Update") + " Copyrights";
}
private String getPath(File file)
{
String path = file.getAbsolutePath().replace('\\', '/');
return path.substring(workTreeLength);
}
private boolean hasString(String string, String[] strings)
{
string = string.toLowerCase();
for (int i = 0; i < strings.length; i++)
{
if (string.indexOf(strings[i]) != -1)
{
return true;
}
}
return false;
}
private boolean hasExtension(File file, String[] extensions)
{
String fileName = file.getName().toLowerCase();
for (int i = 0; i < extensions.length; i++)
{
if (fileName.endsWith(extensions[i]))
{
return true;
}
}
return false;
}
private void handleResult(Shell shell, File workTree, String duration) throws PartInitException
{
String message = "Copyrights missing: " + missingCopyrights.size();
message += "\nCopyrights rewritten: " + rewriteCount;
message += "\nFiles visited: " + fileCount;
message += "\nTime needed: " + duration;
int size = missingCopyrights.size();
if (size != 0)
{
StringBuilder builder = new StringBuilder(message);
message += "\n\nDo you want to open the files with missing copyrights in editors?";
if (MessageDialog.openQuestion(shell, getTitle(), message))
{
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IWorkbenchPage page = WORKBENCH.getActiveWorkbenchWindow().getActivePage();
builder.append("\n");
for (String missingCopyright : missingCopyrights)
{
builder.append("\nMissing: ");
builder.append(missingCopyright);
File externalFile = new File(workTree, missingCopyright);
IFile file = root.getFileForLocation(new Path(externalFile.getAbsolutePath()));
if (file != null && file.isAccessible())
{
IDE.openEditor(page, file);
}
else
{
IFileStore fileStore = EFS.getLocalFileSystem().getStore(externalFile.toURI());
if (fileStore != null)
{
IDE.openEditorOnFileStore(page, fileStore);
}
}
}
Activator.log(new Status(IStatus.WARNING, Activator.PLUGIN_ID, builder.toString()));
}
}
else
{
MessageDialog.openInformation(shell, getTitle(), message);
Activator.log(new Status(IStatus.INFO, Activator.PLUGIN_ID, message));
}
}
private static String[] combineWords(String[] verbs, String[] nouns)
{
List<String> result = new ArrayList<String>();
for (String noun : nouns)
{
for (String verb : verbs)
{
result.add(verb + " " + noun);
if (verb.endsWith("e"))
{
verb += "d";
}
else
{
verb += "ed";
}
result.add(verb + " " + noun);
}
}
return result.toArray(new String[result.size()]);
}
private static String formatDuration(long start)
{
double duration = System.currentTimeMillis() - start;
String unit = "milliseconds";
if (duration > 1000d)
{
duration = duration / 1000d;
unit = "seconds";
if (duration > 60d)
{
duration = duration / 60d;
unit = "minutes";
if (duration > 60d)
{
duration = duration / 60d;
unit = "hours";
}
}
}
duration = Math.round(duration * 100d) / 100d;
return duration + " " + unit;
}
private static void writeLines(File file, List<String> lines) throws IOException
{
Writer fileWriter = null;
BufferedWriter bufferedWriter = null;
try
{
fileWriter = new FileWriter(file);
bufferedWriter = new BufferedWriter(fileWriter);
for (String line : lines)
{
bufferedWriter.write(line);
bufferedWriter.write(NL);
}
}
finally
{
close(bufferedWriter);
close(fileWriter);
}
}
private static void close(Closeable closeable)
{
if (closeable != null)
{
try
{
closeable.close();
}
catch (IOException ex)
{
Activator.log(ex);
}
}
}
}