| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed 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 com.intellij.pom.core.impl; |
| |
| import com.intellij.lang.ASTNode; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.progress.EmptyProgressIndicator; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.ProgressIndicatorProvider; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Disposer; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.UserDataHolderBase; |
| import com.intellij.pom.PomModel; |
| import com.intellij.pom.PomModelAspect; |
| import com.intellij.pom.PomTransaction; |
| import com.intellij.pom.event.PomModelEvent; |
| import com.intellij.pom.event.PomModelListener; |
| import com.intellij.pom.impl.PomTransactionBase; |
| import com.intellij.pom.tree.TreeAspect; |
| import com.intellij.pom.tree.TreeAspectEvent; |
| import com.intellij.psi.*; |
| import com.intellij.psi.codeStyle.CodeStyleManager; |
| import com.intellij.psi.impl.*; |
| import com.intellij.psi.impl.source.PsiFileImpl; |
| import com.intellij.psi.impl.source.text.BlockSupportImpl; |
| import com.intellij.psi.impl.source.text.DiffLog; |
| import com.intellij.psi.impl.source.tree.FileElement; |
| import com.intellij.psi.impl.source.tree.TreeElement; |
| import com.intellij.psi.impl.source.tree.TreeUtil; |
| import com.intellij.psi.text.BlockSupport; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.ThrowableRunnable; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.Stack; |
| import com.intellij.util.lang.CompoundRuntimeException; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| |
| public class PomModelImpl extends UserDataHolderBase implements PomModel { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.pom.core.impl.PomModelImpl"); |
| private final Project myProject; |
| private final Map<Class<? extends PomModelAspect>, PomModelAspect> myAspects = new HashMap<Class<? extends PomModelAspect>, PomModelAspect>(); |
| private final Map<PomModelAspect, List<PomModelAspect>> myIncidence = new HashMap<PomModelAspect, List<PomModelAspect>>(); |
| private final Map<PomModelAspect, List<PomModelAspect>> myInvertedIncidence = new HashMap<PomModelAspect, List<PomModelAspect>>(); |
| private final Collection<PomModelListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| |
| public PomModelImpl(Project project) { |
| myProject = project; |
| } |
| |
| @Override |
| public <T extends PomModelAspect> T getModelAspect(@NotNull Class<T> aClass) { |
| //noinspection unchecked |
| return (T)myAspects.get(aClass); |
| } |
| |
| @Override |
| public void registerAspect(@NotNull Class<? extends PomModelAspect> aClass, @NotNull PomModelAspect aspect, @NotNull Set<PomModelAspect> dependencies) { |
| myAspects.put(aClass, aspect); |
| final Iterator<PomModelAspect> iterator = dependencies.iterator(); |
| final List<PomModelAspect> deps = new ArrayList<PomModelAspect>(); |
| // todo: reorder dependencies |
| while (iterator.hasNext()) { |
| final PomModelAspect depend = iterator.next(); |
| deps.addAll(getAllDependencies(depend)); |
| } |
| deps.add(aspect); // add self to block same aspect transactions from event processing and update |
| for (final PomModelAspect pomModelAspect : deps) { |
| final List<PomModelAspect> pomModelAspects = myInvertedIncidence.get(pomModelAspect); |
| if (pomModelAspects != null) { |
| pomModelAspects.add(aspect); |
| } |
| else { |
| myInvertedIncidence.put(pomModelAspect, new ArrayList<PomModelAspect>(Collections.singletonList(aspect))); |
| } |
| } |
| myIncidence.put(aspect, deps); |
| } |
| |
| //private final Pair<PomModelAspect, PomModelAspect> myHolderPair = new Pair<PomModelAspect, PomModelAspect>(null, null); |
| private List<PomModelAspect> getAllDependencies(PomModelAspect aspect){ |
| List<PomModelAspect> pomModelAspects = myIncidence.get(aspect); |
| return pomModelAspects != null ? pomModelAspects : Collections.<PomModelAspect>emptyList(); |
| } |
| |
| private List<PomModelAspect> getAllDependants(PomModelAspect aspect){ |
| List<PomModelAspect> pomModelAspects = myInvertedIncidence.get(aspect); |
| return pomModelAspects != null ? pomModelAspects : Collections.<PomModelAspect>emptyList(); |
| } |
| |
| @Override |
| public void addModelListener(@NotNull PomModelListener listener) { |
| myListeners.add(listener); |
| } |
| |
| @Override |
| public void addModelListener(@NotNull final PomModelListener listener, @NotNull Disposable parentDisposable) { |
| addModelListener(listener); |
| Disposer.register(parentDisposable, new Disposable() { |
| @Override |
| public void dispose() { |
| removeModelListener(listener); |
| } |
| }); |
| } |
| |
| @Override |
| public void removeModelListener(@NotNull PomModelListener listener) { |
| myListeners.remove(listener); |
| } |
| |
| private final Stack<Pair<PomModelAspect, PomTransaction>> myBlockedAspects = new Stack<Pair<PomModelAspect, PomTransaction>>(); |
| |
| @Override |
| public void runTransaction(@NotNull PomTransaction transaction) throws IncorrectOperationException{ |
| if (!allowPsiModification) { |
| throw new IncorrectOperationException("Must not modify PSI inside save listener"); |
| } |
| List<Throwable> throwables = new ArrayList<Throwable>(0); |
| synchronized(PsiLock.LOCK){ |
| final PomModelAspect aspect = transaction.getTransactionAspect(); |
| startTransaction(transaction); |
| try{ |
| DebugUtil.startPsiModification(null); |
| myBlockedAspects.push(Pair.create(aspect, transaction)); |
| |
| final PomModelEvent event; |
| try{ |
| transaction.run(); |
| event = transaction.getAccumulatedEvent(); |
| } |
| catch(Exception e){ |
| LOG.error(e); |
| return; |
| } |
| finally{ |
| myBlockedAspects.pop(); |
| } |
| final Pair<PomModelAspect,PomTransaction> block = getBlockingTransaction(aspect, transaction); |
| if(block != null){ |
| final PomModelEvent currentEvent = block.getSecond().getAccumulatedEvent(); |
| currentEvent.merge(event); |
| return; |
| } |
| |
| { // update |
| final Set<PomModelAspect> changedAspects = event.getChangedAspects(); |
| final Collection<PomModelAspect> dependants = new LinkedHashSet<PomModelAspect>(); |
| for (final PomModelAspect pomModelAspect : changedAspects) { |
| dependants.addAll(getAllDependants(pomModelAspect)); |
| } |
| for (final PomModelAspect modelAspect : dependants) { |
| if (!changedAspects.contains(modelAspect)) { |
| modelAspect.update(event); |
| } |
| } |
| } |
| for (final PomModelListener listener : myListeners) { |
| final Set<PomModelAspect> changedAspects = event.getChangedAspects(); |
| for (PomModelAspect modelAspect : changedAspects) { |
| if (listener.isAspectChangeInteresting(modelAspect)) { |
| listener.modelChanged(event); |
| break; |
| } |
| } |
| } |
| } |
| catch (Throwable t) { |
| throwables.add(t); |
| } |
| finally { |
| try { |
| commitTransaction(transaction); |
| } |
| catch (Throwable t) { |
| throwables.add(t); |
| } |
| finally { |
| DebugUtil.finishPsiModification(); |
| } |
| } |
| } |
| |
| if (!throwables.isEmpty()) CompoundRuntimeException.doThrow(throwables); |
| } |
| |
| @Nullable |
| private Pair<PomModelAspect,PomTransaction> getBlockingTransaction(final PomModelAspect aspect, PomTransaction transaction) { |
| final List<PomModelAspect> allDependants = getAllDependants(aspect); |
| for (final PomModelAspect pomModelAspect : allDependants) { |
| final ListIterator<Pair<PomModelAspect, PomTransaction>> blocksIterator = myBlockedAspects.listIterator(myBlockedAspects.size()); |
| while (blocksIterator.hasPrevious()) { |
| final Pair<PomModelAspect, PomTransaction> pair = blocksIterator.previous(); |
| if (pomModelAspect == pair.getFirst() && // aspect dependence |
| PsiTreeUtil.isAncestor(pair.getSecond().getChangeScope(), transaction.getChangeScope(), false) && |
| // target scope contain current |
| getContainingFileByTree(pair.getSecond().getChangeScope()) != null // target scope physical |
| ) { |
| return pair; |
| } |
| } |
| } |
| return null; |
| } |
| |
| private void commitTransaction(final PomTransaction transaction) { |
| final ProgressIndicator progressIndicator = ProgressIndicatorProvider.getGlobalProgressIndicator(); |
| final PsiDocumentManagerBase manager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); |
| final PsiToDocumentSynchronizer synchronizer = manager.getSynchronizer(); |
| final PsiFile containingFileByTree = getContainingFileByTree(transaction.getChangeScope()); |
| Document document = containingFileByTree != null ? manager.getCachedDocument(containingFileByTree) : null; |
| if (document != null) { |
| final int oldLength = containingFileByTree.getTextLength(); |
| boolean success = synchronizer.commitTransaction(document); |
| if (success) { |
| BlockSupportImpl.sendAfterChildrenChangedEvent((PsiManagerImpl)PsiManager.getInstance(myProject), containingFileByTree, oldLength, true); |
| } |
| } |
| if (containingFileByTree != null) { |
| boolean isFromCommit = ApplicationManager.getApplication().isDispatchThread() && |
| ApplicationManager.getApplication().hasWriteAction(CommitToPsiFileAction.class); |
| if (!isFromCommit && !synchronizer.isIgnorePsiEvents()) { |
| reparseParallelTrees(containingFileByTree); |
| } |
| } |
| |
| if (progressIndicator != null) progressIndicator.finishNonCancelableSection(); |
| } |
| |
| private void reparseParallelTrees(PsiFile changedFile) { |
| List<PsiFile> allFiles = changedFile.getViewProvider().getAllFiles(); |
| if (allFiles.size() <= 1) { |
| return; |
| } |
| |
| CharSequence newText = changedFile.getNode().getChars(); |
| for (final PsiFile file : allFiles) { |
| if (file != changedFile) { |
| FileElement fileElement = ((PsiFileImpl)file).getTreeElement(); |
| if (fileElement != null) { |
| CharSequence oldText = fileElement.getChars(); |
| reparseFile(file, newText, oldText); |
| } |
| } |
| } |
| } |
| |
| private void reparseFile(final PsiFile file, CharSequence newText, CharSequence oldText) { |
| if (oldText.equals(newText)) return; |
| |
| PsiToDocumentSynchronizer synchronizer =((PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject)).getSynchronizer(); |
| TextRange changedPsiRange = DocumentCommitProcessor.getChangedPsiRange(file, oldText, newText); |
| if (changedPsiRange == null) return; |
| |
| final DiffLog log = BlockSupport.getInstance(myProject).reparseRange(file, changedPsiRange, newText, new EmptyProgressIndicator()); |
| synchronizer.setIgnorePsiEvents(true); |
| try { |
| CodeStyleManager.getInstance(file.getProject()).performActionWithFormatterDisabled(new Runnable() { |
| @Override |
| public void run() { |
| runTransaction(new PomTransactionBase(file, getModelAspect(TreeAspect.class)) { |
| @Nullable |
| @Override |
| public PomModelEvent runInner() throws IncorrectOperationException { |
| return new TreeAspectEvent(PomModelImpl.this, log.performActualPsiChange(file)); |
| } |
| }); |
| } |
| }); |
| } |
| finally { |
| synchronizer.setIgnorePsiEvents(false); |
| } |
| } |
| |
| private void startTransaction(@NotNull PomTransaction transaction) { |
| final ProgressIndicator progressIndicator = ProgressIndicatorProvider.getGlobalProgressIndicator(); |
| if(progressIndicator != null) progressIndicator.startNonCancelableSection(); |
| final PsiDocumentManagerBase manager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); |
| final PsiToDocumentSynchronizer synchronizer = manager.getSynchronizer(); |
| final PsiElement changeScope = transaction.getChangeScope(); |
| LOG.assertTrue(changeScope != null); |
| |
| final PsiFile containingFileByTree = getContainingFileByTree(changeScope); |
| if (changeScope.isPhysical() && synchronizer.toProcessPsiEvent() && isDocumentUncommitted(containingFileByTree)) { |
| // fail-fast to prevent any psi modifications that would cause psi/document text mismatch |
| // PsiToDocumentSynchronizer assertions happen inside event processing and are logged by PsiManagerImpl.fireEvent instead of being rethrown |
| // so it's important to throw something outside event processing |
| throw new IllegalStateException("Attempt to modify PSI for non-committed Document!"); |
| } |
| |
| BlockSupportImpl.sendBeforeChildrenChangeEvent((PsiManagerImpl)PsiManager.getInstance(myProject), changeScope, true); |
| Document document = containingFileByTree == null ? null : manager.getCachedDocument(containingFileByTree); |
| if(document != null) { |
| synchronizer.startTransaction(myProject, document, changeScope); |
| } |
| } |
| |
| private boolean isDocumentUncommitted(@Nullable PsiFile file) { |
| if (file == null) return false; |
| |
| PsiDocumentManager manager = PsiDocumentManager.getInstance(myProject); |
| Document cachedDocument = manager.getCachedDocument(file); |
| return cachedDocument != null && manager.isUncommited(cachedDocument); |
| } |
| |
| @Nullable |
| private static PsiFile getContainingFileByTree(@NotNull final PsiElement changeScope) { |
| // there could be pseudo physical trees (JSPX/JSP/etc.) which must not translate |
| // any changes to document and not to fire any PSI events |
| final PsiFile psiFile; |
| final ASTNode node = changeScope.getNode(); |
| if (node == null) { |
| psiFile = changeScope.getContainingFile(); |
| } |
| else { |
| final FileElement fileElement = TreeUtil.getFileElement((TreeElement)node); |
| // assert fileElement != null : "Can't find file element for node: " + node; |
| // Hack. the containing tree can be invalidated if updating supplementary trees like HTML in JSP. |
| if (fileElement == null) return null; |
| |
| psiFile = (PsiFile)fileElement.getPsi(); |
| } |
| return psiFile.getNode() != null ? psiFile : null; |
| } |
| |
| private static volatile boolean allowPsiModification = true; |
| public static <T extends Throwable> void guardPsiModificationsIn(@NotNull ThrowableRunnable<T> runnable) throws T { |
| ApplicationManager.getApplication().assertWriteAccessAllowed(); |
| boolean old = allowPsiModification; |
| try { |
| allowPsiModification = false; |
| runnable.run(); |
| } |
| finally { |
| allowPsiModification = old; |
| } |
| } |
| } |