/* * Copyright (c) 2014, IETR/INSA of Rennes * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the IETR/INSA of Rennes nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ package net.sf.orcc.ui.refactoring; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sf.orcc.util.FilesManager; import net.sf.orcc.util.OrccUtil; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.CompositeChange; import org.eclipse.ltk.core.refactoring.TextFileChange; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; /** * * @author Antoine Lorence * */ public class ChangesFactory { interface Replacement { /** * Check if the given text contains the pattern of this Replacement * instance. * * @param content * @return */ boolean isAffected(final String content); /** * Get the list of ReplaceEdit corresponding to the replacements * represented by this instance. * * @param content * @return */ List<ReplaceEdit> getReplacements(final String content); } class StandardReplacement implements Replacement { private String pattern; private String replacement; public StandardReplacement(String p, String r) { pattern = p; replacement = r; } @Override public boolean isAffected(final String content) { return content.contains(pattern); } @Override public List<ReplaceEdit> getReplacements(final String content) { final List<ReplaceEdit> replacements = new ArrayList<ReplaceEdit>(); int idx = 0; while ((idx = content.indexOf(pattern, idx + 1)) != -1) { replacements.add(new ReplaceEdit(idx, pattern.length(), replacement)); idx += pattern.length(); } return replacements; } } class RegexpReplacement implements Replacement { private Pattern pattern; private String replacement; public RegexpReplacement(Pattern p, String r) { pattern = p; replacement = r; } @Override public boolean isAffected(final String content) { return pattern.matcher(content).find(); } @Override public List<ReplaceEdit> getReplacements(final String content) { final List<ReplaceEdit> replacements = new ArrayList<ReplaceEdit>(); final Matcher matcher = pattern.matcher(content); while (matcher.find()) { int s = matcher.start(); int e = matcher.end(); final String replaced = pattern .matcher(content.substring(s, e)).replaceAll( replacement); replacements.add(new ReplaceEdit(s, e - s, replaced)); } return replacements; } } /** * The list of replacements to apply, indexed by the suffix of the files * whose can be affected by these replacements */ final private Multimap<String, Replacement> replacements; /** * A map of TextEdit objects related to a file in the workspace. */ final private Map<IFile, TextEdit> results; public ChangesFactory() { replacements = HashMultimap.create(); results = new HashMap<IFile, TextEdit>(); } public void addReplacement(final String suffix, final Pattern pattern, final String replacement) { replacements.put(suffix, new RegexpReplacement(pattern, replacement)); } public void addReplacement(final String suffix, final String pattern, final String replacement) { replacements.put(suffix, new StandardReplacement(pattern, replacement)); } public void addSpecificFileReplacement(final IFile file, final Pattern pattern, final String repl) { final Replacement replacement = new RegexpReplacement(pattern, repl); final String content = FilesManager.readFile(file.getRawLocation() .toString()); final TextEdit edits = getTextEdit(file); if (replacement.isAffected(content)) { for (final ReplaceEdit edit : replacement.getReplacements(content)) { edits.addChild(edit); } } } public void addSpecificFileReplacement(final IFile file, final String pattern, final String repl) { final Replacement replacement = new StandardReplacement(pattern, repl); final String content = FilesManager.readFile(file.getRawLocation() .toString()); final TextEdit edits = getTextEdit(file); if (replacement.isAffected(content)) { for (final ReplaceEdit edit : replacement.getReplacements(content)) { edits.addChild(edit); } } } public void clearConfiguration() { replacements.clear(); } public void resetResults() { results.clear(); } /** * Generates a Change object with the following rules: * <ol> * <li>Computes the list of all files in the given project and the projects * depending on it.</li> * <li>Apply to each file the previously stored replacements</li> * <li>Compute the results in a CompositeChange instance containing a list * of TextFileChanges instances (1 per file)</li> * </ol> * * The given title is used to set a name to the created Change. <b>Please * note: </b> This method will returns null if the current configuration * doesn't imply any Change in the given project's files. * * @param project * @param title * @return a Change instance, or null */ public Change getAllChanges(final IProject project, final String title) { return getAllChanges(project, title, Collections.<IFile>emptyList()); } /** * Generates a Change object with the following rules: * <ol> * <li>Computes the list of all files in the given project and the projects * depending on it.</li> * <li>Removes from this list all files in the given ignoreList.</li> * <li>Apply to each file the previously stored replacements</li> * <li>Compute the results in a CompositeChange instance containing a list * of TextFileChanges instances (1 per file)</li> * </ol> * * The given title is used to set a name to the created Change. <b>Please * note: </b> This method will returns null if the current configuration * doesn't imply any Change in the given project's files. * * @param project * @param title * @param ignoreList * @return a Change instance, or null */ public Change getAllChanges(final IProject project, final String title, final Collection<IFile> ignoreList) { final List<IFolder> folders = OrccUtil .getAllDependingSourceFolders(project); List<IFile> files; for (String suffix : replacements.keySet()) { files = OrccUtil.getAllFiles(suffix, folders); files.removeAll(ignoreList); for (IFile file : files) { computeResults(file, replacements.get(suffix)); } } return getFinalresult(title); } /** * Generates a Change object with the following rules: * <ol> * <li>Apply to each file in the given list the transformations stored in * the internal replacements map</li> * <li>Compute the results in a CompositeChange instance containing a list * of TextFileChanges instances (1 per file)</li> * </ol> * * @param files * @param title * @return */ public Change getAllChanges(final Collection<IFile> files, final String title) { for (IFile file : files) { final String suffix = file.getFileExtension(); if(suffix != null) { computeResults(file, replacements.get(suffix)); } } return getFinalresult(title); } /** * Return the existing TextEdit associated with the given file, creates it * if necessary. * * @param file * @return */ private TextEdit getTextEdit(final IFile file) { TextEdit textEdit = results.get(file); if (textEdit == null) { textEdit = new MultiTextEdit(); results.put(file, textEdit); } return textEdit; } /** * Check if the given file is affected by at least 1 of the given * replacements, and fill the internal result map entry with the * corresponding ReplaceEdit instances * * @param file * @param replacements */ private void computeResults(final IFile file, final Collection<Replacement> replacements) { if (!file.exists()) { return; } final String content = FilesManager.readFile(file.getRawLocation() .toString()); for (Replacement replaceInfo : replacements) { if (replaceInfo.isAffected(content)) { final TextEdit textEdit = getTextEdit(file); for (ReplaceEdit replaceEdit : replaceInfo .getReplacements(content)) { textEdit.addChild(replaceEdit); } } } } /** * Compute a CompositeChange object from the previously computed TextEdit * instances encapsulated in new TextFileChange instances. * * @param title The title set to the resulting Change object * @return */ private Change getFinalresult(final String title) { final CompositeChange result = new CompositeChange(title); for (Entry<IFile, TextEdit> entry : results.entrySet()) { final IFile file = entry.getKey(); final TextFileChange fileChange = new TextFileChange("Changes to " + file.getName(), file); fileChange.setEdit(entry.getValue()); result.add(fileChange); } return result.getChildren().length > 0 ? result : null; } }