blob: e63fd66ea52fe922c5d1369d8f4274297904fc7c [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.form.refactoring;
import java.awt.EventQueue;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.modules.form.FormDataObject;
import org.netbeans.modules.form.FormEditor;
import org.netbeans.modules.form.PersistenceException;
import org.netbeans.modules.form.RADComponent;
import org.netbeans.modules.form.RenameSupport;
import org.netbeans.modules.form.ResourceSupport;
import org.netbeans.modules.nbform.FormEditorSupport;
import org.netbeans.modules.refactoring.api.SingleCopyRefactoring;
import org.netbeans.modules.refactoring.spi.BackupFacility;
import org.netbeans.modules.refactoring.spi.RefactoringElementImplementation;
import org.netbeans.modules.refactoring.spi.SimpleRefactoringElementImplementation;
import org.netbeans.modules.refactoring.spi.Transaction;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.text.PositionBounds;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
/**
* This class does the actual refactoring changes for one form - updates the
* form, regenerates code, updates properties files for i18n, etc. Multiple
* different instances (updates) can be created and executed for one refactoring
* (all kept in RefacoringInfo).
*
* @author Tomas Pavek
*/
public class FormRefactoringUpdate extends SimpleRefactoringElementImplementation implements Transaction {
/**
* Information about the performed refactoring.
*/
private RefactoringInfo refInfo;
/**
* RefactoringElement used in the preview, but doing nothing.
*/
private RefactoringElementImplementation previewElement;
/**
* Java file of a form affected by the refactoring.
*/
private FileObject changingFile;
/**
* DataObject of the changed file. Has changedFile as primary file at the
* beginning, but may get a different one later (e.g. if moved).
*/
private FormDataObject formDataObject;
/**
* FormEditor of the updated form. Either taken from the FormDataObject
* (typically when already opened), or created temporarily just to do the
* update. See prepareForm method.
*/
private FormEditor formEditor;
private boolean loadingFailed;
/**
* Whether a change in guarded code was requested by java refactoring.
*/
private boolean guardedCodeChanging;
private boolean transactionDone;
private boolean formFileRenameDone;
private List<BackupFacility.Handle> backups;
// -----
public FormRefactoringUpdate(RefactoringInfo refInfo, FileObject changingFile) {
this.refInfo = refInfo;
this.changingFile = changingFile;
try {
DataObject dobj = DataObject.find(changingFile);
if (dobj instanceof FormDataObject) {
formDataObject = (FormDataObject) dobj;
}
} catch(DataObjectNotFoundException ex) {
assert false;
}
}
FormDataObject getFormDataObject() {
return formDataObject;
}
RefactoringElementImplementation getPreviewElement(/*String displayText*/) {
if (previewElement == null) {
previewElement = new PreviewElement(changingFile/*, displayText*/);
}
return previewElement;
}
void setGaurdedCodeChanging(boolean b) {
guardedCodeChanging = b;
}
boolean isGuardedCodeChanging() {
return guardedCodeChanging;
}
// -----
// Transaction (registered via RefactoringElementsBag.registerTransaction)
@Override
public void commit() {
if (previewElement != null && !previewElement.isEnabled()) {
return;
}
// As "transactions" we do updates for changes affecting only the
// content of the source file, not changing the file's name or location.
// Our transaction is called after retouche commits its changes to the
// source. After all transactions are done, the source file is saved
// automatically.
for (FileObject originalFile : refInfo.getOriginalFiles()) {
// (Actually more original files make only sense for this form if
// they represent components used in this form and moved...)
switch (refInfo.getChangeType()) {
case VARIABLE_RENAME: // renaming a variable (just one)
if (originalFile.equals(changingFile)) {
renameMetaComponent(refInfo.getOldName(originalFile), refInfo.getNewName());
transactionDone = true;
}
break;
case CLASS_RENAME: // renaming a component class used in the form (just one)
if (!originalFile.equals(changingFile)) {
componentClassRename(originalFile);
transactionDone = true;
}
break;
case CLASS_MOVE: // moving a class used in the form (there can be more of them)
if (!originalFile.equals(changingFile) && isGuardedCodeChanging()) {
componentChange(refInfo.getOldName(originalFile), refInfo.getNewName(originalFile));
transactionDone = !refInfo.containsOriginalFile(changingFile);
// If a form is moved together with other java classes, it needs
// to be checked here but also processed later in performChange
// method. If it contained some of the moved components, we will
// not be able to load it. But that is not for sure, so will try
// anyway, at worst it won't be processed.
}
break;
case CLASS_DELETE: // deleting form (more can be deleted, but here we only care about this form)
if (originalFile.equals(changingFile)) {
saveFormForUndo(); // we only need to backup the form file for undo
transactionDone = true;
}
break;
case PACKAGE_RENAME:
case FOLDER_RENAME: // renaming package of a component used in the form,
// but not the package of the form itself
// (just one package renamed)
if (!changingFile.getParent().equals(originalFile)) {
packageRename(originalFile);
transactionDone = true;
}
break;
default:
// do nothing otherwise - could be just redundantly registered by the guarded handler
return;
}
}
}
// Transaction (registered via RefactoringElementsBag.registerTransaction)
@Override
public void rollback() {
if (previewElement != null && !previewElement.isEnabled()) {
return;
}
undoFromBackups();
/* switch (refInfo.getChangeType()) {
case VARIABLE_RENAME:
renameMetaComponent(refInfo.getNewName(), refInfo.getOldName());
break;
case CLASS_RENAME: // renaming a component class used in the form
if (!refInfo.getPrimaryFile().equals(changingFile)) {
componentClassRename(refInfo.getNewName(), refInfo.getOldName());
}
break;
case CLASS_MOVE: // moving a component class used in the form
if (!refInfo.getPrimaryFile().equals(changingFile)) {
componentChange(refInfo.getNewName(), refInfo.getOldName());
}
break;
} */
}
// RefactoringElementImplementation (registered via RefactoringElementsBag.addFileChange)
@Override
public void performChange() {
if (previewElement != null && !previewElement.isEnabled()) {
return;
}
if (transactionDone) { // could be registered redundantly as file change
processCustomCode();
return;
}
// As "file changes" we do updates that react on changes of the source
// file's name or location. We need the source file to be already
// renamed/moved. The file changes are run after the "transactions".
for (FileObject originalFile : refInfo.getOriginalFiles()) {
// Looking through if this form is among original files - i.e. the
// one being changed.
switch (refInfo.getChangeType()) {
case CLASS_RENAME: // renaming the form itself
if (originalFile.equals(changingFile)) {
formRename();
}
break;
case CLASS_MOVE: // moving the form itself
if (originalFile.equals(changingFile) && prepareForm(false)) {
formMove();
}
break;
case CLASS_COPY: // copying the form itslef
if (originalFile.equals(changingFile) && prepareForm(false)) {
formCopy();
}
break;
case PACKAGE_RENAME: // renaming package of the form (just one)
case FOLDER_RENAME:
packageRename(originalFile);
break;
}
}
processCustomCode();
}
// RefactoringElementImplementation (registered via RefactoringElementsBag.addFileChange)
@Override
public void undoChange() {
if (previewElement != null && !previewElement.isEnabled()) {
return;
}
if (transactionDone) { // could be registered redundantly as file change
return;
}
undoFromBackups();
}
// -----
private void renameMetaComponent(String oldName, String newName) {
if (prepareForm(true)) {
RADComponent metacomp = formEditor.getFormModel().findRADComponent(oldName);
if (metacomp != null) {
saveFormForUndo();
saveResourcesForContentChangeUndo();
metacomp.setName(newName);
updateForm(false);
}
}
}
private void formRename() {
if (prepareForm(true)) {
saveFormForUndo();
saveResourcesForFormRenameUndo();
ResourceSupport.formMoved(formEditor.getFormModel(), null, refInfo.getOldName(changingFile), false);
updateForm(true);
}
}
private void componentClassRename(FileObject originalFile) {
String oldName = refInfo.getOldName(originalFile);
String newName = refInfo.getNewName();
String pkg = ClassPath.getClassPath(originalFile, ClassPath.SOURCE)
.getResourceName(originalFile.getParent(), '.', false);
String oldClassName = (pkg != null && pkg.length() > 0)
? pkg + "." + oldName : oldName; // NOI18N
String newClassName = (pkg != null && pkg.length() > 0)
? pkg + "." + newName : newName; // NOI18N
componentChange(oldClassName, newClassName);
}
private FormEditorSupport getFormEditorSupport() {
return (FormEditorSupport)formDataObject.getFormEditorSupport();
}
private void formMove(/*final boolean saveAll*/) {
final FormEditorSupport fes = getFormEditorSupport();
if (fes.isOpened()) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
formEditor = fes.reloadFormEditor();
formMove2(/*saveAll*/);
}
});
} else {
assert !formEditor.isFormLoaded();
formMove2(/*saveAll*/);
}
}
private void formMove2(/*boolean saveAll*/) {
if (prepareForm(true)) {
saveFormForUndo();
FileObject oldFolder = changingFile.getParent();
saveResourcesForFormMoveUndo(oldFolder);
String oldFormName = refInfo.getOldName(changingFile);
oldFormName = oldFormName.substring(oldFormName.lastIndexOf('.')+1); // should be a short name
ResourceSupport.formMoved(formEditor.getFormModel(), oldFolder, oldFormName, false);
updateForm(true);
}
}
private void formCopy() {
if (refInfo.getRefactoring() instanceof SingleCopyRefactoring) {
FileObject oldFile = changingFile;
FormDataObject oldForm = formDataObject;
FileObject oldFolder = changingFile.getParent();
SingleCopyRefactoring copyRef = (SingleCopyRefactoring)refInfo.getRefactoring();
String newName = copyRef.getNewName(); // short name without extension
Lookup target = copyRef.getTarget();
FileObject targetFolder = URLMapper.findFileObject((URL)target.lookup(URL.class));
// will process the new copy - update changingFile and formDataObject fields
changingFile = targetFolder.getFileObject(newName, "java"); // NOI18N
try {
DataObject dobj = DataObject.find(changingFile);
if (dobj instanceof FormDataObject) {
formDataObject = (FormDataObject) dobj;
}
} catch(DataObjectNotFoundException ex) {
assert false;
}
formEditor = null;
if (prepareForm(true)) {
saveResourcesForFormRenameUndo(); // same set of files like if the new form was renamed
if (oldFolder == targetFolder) {
oldFolder = null;
}
ResourceSupport.formMoved(formEditor.getFormModel(), oldFolder, oldFile.getName(), true);
updateForm(true);
}
// set back to original so the operation can be repeated in redo
changingFile = oldFile;
formDataObject = oldForm;
}
}
private void componentChange(String oldClassName, String newClassName) {
if (oldClassName == null || newClassName == null) {
return; // for unknown reason 'newClassName' is sometimes null during move refactoring, issue 174136
}
FormEditorSupport fes = getFormEditorSupport();
if (fes.isOpened()) {
fes.closeFormEditor();
}
String[] oldNames = new String[] { oldClassName };
String[] newNames = new String[] { newClassName };
replaceClassOrPkgName(oldNames, newNames, false);
replaceShortClassName(oldNames, newNames);
// (Only updating form file, java code gets updated via GuardedBlockUpdate)
}
private boolean replaceShortClassName(String[] oldNames, String[] newNames) {
List<String> oldList = new LinkedList<String>();
List<String> newList = new LinkedList<String>();
for (int i=0; i < oldNames.length; i++) {
if (oldNames[i].contains(".")) { // NOI18N
String shortOldName = oldNames[i].substring(oldNames[i].lastIndexOf('.')+1);
String shortNewName = newNames[i].substring(newNames[i].lastIndexOf('.')+1);
if (!shortNewName.equals(shortOldName)) {
oldList.add(shortOldName);
newList.add(newNames[i]); // intentionally replace with FQN
}
}
}
if (!oldList.isEmpty()) {
oldNames = oldList.toArray(new String[oldList.size()]);
newNames = newList.toArray(new String[newList.size()]);
return replaceClassOrPkgName(oldNames, newNames, false);
}
return false;
}
private void packageRename(FileObject originalPkgFile) {
FormEditorSupport fes = getFormEditorSupport();
if (fes.isOpened()) {
fes.closeFormEditor();
}
String oldName = refInfo.getOldName(originalPkgFile);
String newName = refInfo.getNewName();
if (refInfo.getChangeType() == RefactoringInfo.ChangeType.FOLDER_RENAME) {
// determine full package name for renamed folder
ClassPath cp = ClassPath.getClassPath(originalPkgFile, ClassPath.SOURCE);
FileObject parent = originalPkgFile.getParent();
if (cp != null && cp.contains(parent)) {
String parentPkgName = cp.getResourceName(parent, '.', false);
if (parentPkgName != null && parentPkgName.length() > 0) {
oldName = parentPkgName + "." + oldName; // NOI18N
newName = parentPkgName + "." + newName; // NOI18N
}
}
}
if (replaceClassOrPkgName(new String[] { oldName },
new String[] { newName },
true)
&& !isGuardedCodeChanging()) {
// some package references in resource were changed in the form file
// (not class names since no change in guarded code came from java
// refactoring) and because no component has changed we can load the
// form and regenerate to get the new resource names into code
updateForm(true);
}
}
/**
* Tries to update the fragments of custom code in the .form file according
* to the refactoring change. The implementation is quite simple and
* super-ugly. It goes through the form file, finds relevant attributes,
* and blindly replaces given "old name" with a "new name". Should mostly
* work when a component variable or class is renamed. Should be enough
* though, since the usage of custom code is quite limited.
*/
private void processCustomCode() {
if (isGuardedCodeChanging() && !formFileRenameDone) {
boolean replaced = false;
List<String> oldList = new LinkedList<String>();
List<String> newList = new LinkedList<String>();
for (FileObject originalFile : refInfo.getOriginalFiles()) {
String oldName = refInfo.getOldName(originalFile);
String newName = refInfo.getNewName(originalFile);
if (oldName != null && newName != null) {
oldList.add(oldName);
newList.add(newName);
}
}
if (!oldList.isEmpty()) {
String[] oldNames = oldList.toArray(new String[oldList.size()]);
String[] newNames = newList.toArray(new String[newList.size()]);
replaced |= replaceClassOrPkgName(oldNames, newNames, false);
// also try to replace short class name
switch (refInfo.getChangeType()) {
case CLASS_RENAME:
case CLASS_MOVE:
replaced |= replaceShortClassName(oldNames, newNames);
break;
}
}
if (replaced) { // regenerate the code
// need to reload the form from file
final FormEditorSupport fes = getFormEditorSupport();
if (fes.isOpened()) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
formEditor = fes.reloadFormEditor();
updateForm(true);
}
});
} else {
if (formEditor != null && formEditor.isFormLoaded()) {
formEditor.closeForm();
}
if (prepareForm(true)) {
updateForm(true);
}
}
}
formFileRenameDone = false; // not to block redo
}
}
// -----
/**
* Regenerate code and save.
*/
private void updateForm(boolean saveAll) {
if (!prepareForm(true)) {
return;
}
// hack: regenerate code immediately
formEditor.getFormModel().fireFormChanged(true);
FormEditorSupport fes = getFormEditorSupport();
try {
if (!fes.isOpened()) {
// the form is not opened, just loaded aside to do this refactoring
// update (not held from FormEditorSupport); so we must save the
// form always - it would not get save with refactoring
formEditor.saveFormData(); // TODO should save form only if there was a change
if (saveAll) { // a post-refactoring change that would not be saved by refactoring
fes.saveSourceOnly();
}
formEditor.closeForm();
} else if (saveAll) { // a post-refactoring change that would not be saved
fes.saveDocument();
}
} catch (PersistenceException pex) {
Exceptions.printStackTrace(pex);
} catch (IOException ioex) {
Exceptions.printStackTrace(ioex);
}
}
boolean prepareForm(boolean load) {
if (formDataObject != null) {
FormEditor fe = getFormEditorSupport().getFormEditor();
if (fe != null) { // use the current FormEditor (might change due to reload after undo)
formEditor = fe;
} else if (formEditor == null) { // create a disconnected form editor
formEditor = new FormEditor(formDataObject, formDataObject.getFormEditorSupport());
}
}
if (formEditor != null) {
if (formEditor.isFormLoaded() || !load) {
return true;
} else if (!loadingFailed) {
if (formEditor.loadForm()) {
if (formEditor.anyPersistenceError()) { // Issue 128504
formEditor.closeForm();
loadingFailed = true;
} else {
return true;
}
} else {
loadingFailed = true;
}
}
}
return false;
}
private void saveFormForUndo() {
if (!formDataObject.isValid()) {
// 210787: Refresh formDataObject if it became obsolete
FileObject fob = formDataObject.getPrimaryFile();
if (!fob.isValid()) {
File file = FileUtil.toFile(fob);
fob = FileUtil.toFileObject(file);
}
try {
formDataObject = (FormDataObject)DataObject.find(fob);
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
}
saveForUndo(formDataObject.getFormFile());
// java file is backed up by java refactoring
}
private void saveResourcesForContentChangeUndo() {
for (URL url : ResourceSupport.getFilesForContentChangeBackup(formEditor.getFormModel())) {
saveForUndo(url);
}
}
private void saveResourcesForFormRenameUndo() {
for (URL url : ResourceSupport.getFilesForFormRenameBackup(formEditor.getFormModel())) {
saveForUndo(url);
}
}
private void saveResourcesForFormMoveUndo(FileObject oldFolder) {
for (URL url : ResourceSupport.getFilesForFormMoveBackup(formEditor.getFormModel(), oldFolder)) {
saveForUndo(url);
}
}
private void saveForUndo(final URL url) {
FileObject file = URLMapper.findFileObject(url);
BackupFacility.Handle id;
if (file != null) {
try {
id = BackupFacility.getDefault().backup(file);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
return;
}
} else { // file does not exist - will be created; to undo we must delete it
id = new BackupFacility.Handle() {
@Override
public void restore() throws IOException {
FileObject file = URLMapper.findFileObject(url);
if (file != null) {
file.delete();
}
}
};
}
if (backups == null) {
backups = new ArrayList<BackupFacility.Handle>();
}
backups.add(id);
}
private void saveForUndo(FileObject file) {
try {
BackupFacility.Handle id = BackupFacility.getDefault().backup(file);
if (backups == null) {
backups = new ArrayList<BackupFacility.Handle>();
}
backups.add(id);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
private void undoFromBackups() {
if (backups != null) {
try {
for (BackupFacility.Handle id : backups) {
id.restore();
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
backups.clear();
}
}
// -----
private static class PreviewElement extends SimpleRefactoringElementImplementation {
private FileObject file;
PreviewElement(FileObject file) {
this.file = file;
}
@Override
public String getText() {
return "GUI form update"; // NOI18N
}
@Override
public String getDisplayText() {
return NbBundle.getMessage(FormRefactoringUpdate.class, "CTL_RefactoringUpdate1"); // NOI18N
}
@Override
public void performChange() {
}
@Override
public Lookup getLookup() {
return Lookup.EMPTY;
}
@Override
public FileObject getParentFile() {
return file;
}
@Override
public PositionBounds getPosition() {
return null;
}
}
// -----
// RefactoringElementImplementation
@Override
public String getText() {
return "GUI form update";
}
// RefactoringElementImplementation
@Override
public String getDisplayText() {
return NbBundle.getMessage(FormRefactoringUpdate.class, "CTL_RefactoringUpdate2"); // NOI18N
}
// RefactoringElementImplementation
@Override
public Lookup getLookup() {
return Lookup.EMPTY;
}
// RefactoringElementImplementation
@Override
public FileObject getParentFile() {
return changingFile;
}
// RefactoringElementImplementation
@Override
public PositionBounds getPosition() {
return null;
}
// -----
private boolean replaceClassOrPkgName(String[] oldNames, String[] newNames, boolean pkgName) {
FileObject formFile = formDataObject.getFormFile();
FileLock lock = null;
OutputStream os = null;
try {
lock = formFile.lock();
String outString = RenameSupport.renameInFormFile(formFile, oldNames, newNames, pkgName);
if (outString != null) {
saveForUndo(formFile);
os = formFile.getOutputStream(lock);
os.write(outString.getBytes("UTF-8")); // NOI18N
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
return false;
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException ex) { // ignore
}
if (lock != null) {
lock.releaseLock();
}
}
formFileRenameDone = true; // we don't need to do processCustomCode
return true;
}
}