| /* |
| * 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.maven.queries; |
| |
| import com.sun.source.tree.CompilationUnitTree; |
| import com.sun.source.tree.ModuleTree; |
| import com.sun.source.tree.Tree; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.ChangeListener; |
| import org.netbeans.api.annotations.common.CheckForNull; |
| import org.netbeans.api.annotations.common.NonNull; |
| import org.netbeans.api.annotations.common.NullAllowed; |
| import org.netbeans.api.java.queries.SourceLevelQuery; |
| import org.netbeans.api.java.source.JavaSource; |
| import org.netbeans.modules.maven.NbMavenProjectImpl; |
| import org.netbeans.modules.maven.api.NbMavenProject; |
| import static org.netbeans.modules.maven.classpath.ClassPathProviderImpl.MODULE_INFO_JAVA; |
| import org.netbeans.spi.java.queries.CompilerOptionsQueryImplementation; |
| import org.openide.filesystems.FileAttributeEvent; |
| import org.openide.filesystems.FileChangeListener; |
| import org.openide.filesystems.FileEvent; |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileRenameEvent; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.modules.SpecificationVersion; |
| import org.openide.util.ChangeSupport; |
| import org.openide.util.Parameters; |
| import org.openide.util.WeakListeners; |
| |
| /** |
| * Implementation of the {@link CompilerOptionsQueryImplementation} for unit tests. |
| * @author Tomas Zezula |
| * @author Tomas Stupka |
| * @see org.netbeans.modules.java.api.common.queries.UnitTestsCompilerOptionsQueryImpl |
| */ |
| public final class UnitTestsCompilerOptionsQueryImpl implements CompilerOptionsQueryImplementation { |
| private static final Logger LOG = Logger.getLogger(UnitTestsCompilerOptionsQueryImpl.class.getName()); |
| private static final SpecificationVersion JDK9 = new SpecificationVersion("9"); //NOI18N |
| |
| private final AtomicReference<ResultImpl> result; |
| private final NbMavenProjectImpl proj; |
| |
| public UnitTestsCompilerOptionsQueryImpl( |
| NbMavenProjectImpl proj) { |
| Parameters.notNull("proj", proj); //NOI18N |
| this.proj = proj; |
| this.result = new AtomicReference<>(); |
| } |
| |
| @CheckForNull |
| @Override |
| public Result getOptions(@NonNull final FileObject file) { |
| for (String r : proj.getOriginalMavenProject().getTestCompileSourceRoots()) { |
| FileObject root = FileUtil.toFileObject(new File(r)); |
| if (root != null && isArtifact(root, file)) { |
| ResultImpl res = result.get(); |
| if (res == null) { |
| res = new ResultImpl(proj); |
| if (!result.compareAndSet(null, res)) { |
| res = result.get(); |
| } |
| assert res != null; |
| } |
| return res; |
| } |
| } |
| return null; |
| } |
| |
| private static boolean isArtifact( |
| @NonNull final FileObject root, |
| @NonNull final FileObject file) { |
| return root.equals(file) || FileUtil.isParentOf(root, file); |
| } |
| |
| private static final class ResultImpl extends Result implements ChangeListener, |
| PropertyChangeListener, FileChangeListener { |
| private final ChangeSupport cs; |
| private final ThreadLocal<Boolean> reenter; |
| private final Collection</*@GuardedBy("this")*/File> moduleInfoListeners ; |
| //@GuardedBy("this") |
| private List<String> cache; |
| //@GuardedBy("this") |
| private SourceLevelQuery.Result sourceLevel; |
| //@GuardedBy("this") |
| private boolean listensOnRoots; |
| private final NbMavenProjectImpl proj; |
| |
| ResultImpl(NbMavenProjectImpl proj) { |
| this.proj = proj; |
| this.cs = new ChangeSupport(this); |
| this.reenter = new ThreadLocal<>(); |
| this.moduleInfoListeners = new HashSet<>(); |
| } |
| |
| @Override |
| public List<? extends String> getArguments() { |
| List<String> args; |
| SourceLevelQuery.Result[] slq = new SourceLevelQuery.Result[1]; |
| synchronized (this) { |
| args = cache; |
| slq[0] = sourceLevel; |
| } |
| if (args == null) { |
| if (reenter.get() == Boolean.TRUE) { |
| args = Collections.emptyList(); |
| } else { |
| reenter.set(Boolean.TRUE); |
| try { |
| TestMode mode; |
| final Collection<File> allRoots = new HashSet<>(); |
| final FileObject srcModuleInfo = findModuleInfo(proj.getOriginalMavenProject().getCompileSourceRoots(), allRoots, null); |
| final FileObject testModuleInfo = findModuleInfo(proj.getOriginalMavenProject().getTestCompileSourceRoots(), allRoots, slq); |
| final boolean isLegacy = Optional.ofNullable(slq[0]) |
| .map((r) -> r.getSourceLevel()) |
| .map((sl) -> JDK9.compareTo(new SpecificationVersion(sl)) > 0) |
| .orElse(Boolean.TRUE); |
| mode = isLegacy ? |
| TestMode.LEGACY : |
| srcModuleInfo == null ? |
| TestMode.UNNAMED : |
| testModuleInfo == null ? |
| TestMode.INLINED: |
| TestMode.MODULE; |
| args = mode.createArguments(srcModuleInfo, testModuleInfo); |
| synchronized (this) { |
| if (cache == null) { |
| cache = args; |
| } else { |
| args = cache; |
| } |
| if (sourceLevel == null && slq[0] != null) { |
| sourceLevel = slq[0]; |
| if (sourceLevel.supportsChanges()) { |
| sourceLevel.addChangeListener(WeakListeners.change(this, sourceLevel)); |
| } |
| } |
| if (!listensOnRoots) { |
| listensOnRoots = true; |
| NbMavenProject watcher = proj.getLookup().lookup(NbMavenProject.class); |
| watcher.addPropertyChangeListener(WeakListeners.propertyChange(this, watcher)); |
| } |
| final Set<File> toRemove = new HashSet<>(moduleInfoListeners); |
| toRemove.removeAll(allRoots); |
| allRoots.removeAll(moduleInfoListeners); |
| for (File f : toRemove) { |
| FileUtil.removeFileChangeListener( |
| this, |
| new File(f, MODULE_INFO_JAVA)); |
| moduleInfoListeners.remove(f); |
| } |
| for (File f : allRoots) { |
| FileUtil.addFileChangeListener( |
| this, |
| new File(f, MODULE_INFO_JAVA)); |
| moduleInfoListeners.add(f); |
| } |
| } |
| } finally { |
| reenter.remove(); |
| } |
| } |
| } |
| return args; |
| } |
| |
| @Override |
| public void addChangeListener(@NonNull final ChangeListener listener) { |
| cs.addChangeListener(listener); |
| } |
| |
| @Override |
| public void removeChangeListener(@NonNull final ChangeListener listener) { |
| cs.removeChangeListener(listener); |
| } |
| |
| @Override |
| public void stateChanged(@NonNull final ChangeEvent e) { |
| reset(); |
| } |
| |
| @Override |
| public void propertyChange(@NonNull final PropertyChangeEvent evt) { |
| if (NbMavenProject.PROP_PROJECT.equals(evt.getPropertyName())) { |
| reset(); |
| } |
| } |
| |
| @Override |
| public void fileDataCreated(FileEvent fe) { |
| reset(); |
| } |
| |
| @Override |
| public void fileChanged(FileEvent fe) { |
| reset(); |
| } |
| |
| @Override |
| public void fileDeleted(FileEvent fe) { |
| reset(); |
| } |
| |
| @Override |
| public void fileRenamed(FileRenameEvent fe) { |
| reset(); |
| } |
| |
| @Override |
| public void fileFolderCreated(FileEvent fe) { |
| //Not important |
| } |
| |
| @Override |
| public void fileAttributeChanged(FileAttributeEvent fe) { |
| //Not important |
| } |
| |
| private void reset() { |
| synchronized (this) { |
| cache = null; |
| } |
| cs.fireChange(); |
| } |
| |
| private FileObject findModuleInfo( |
| List<String> roots, |
| @NonNull final Collection<? super File> rootCollector, |
| @NullAllowed final SourceLevelQuery.Result[] holder) { |
| |
| FileObject result = null; |
| for (String rootPath : roots) { |
| File rootFile = new File(rootPath); |
| FileObject rootFileObject = FileUtil.toFileObject(rootFile); |
| if(rootFile.exists() && rootFileObject != null) { |
| if (holder != null) { |
| if (holder[0] == null) { |
| holder[0] = SourceLevelQuery.getSourceLevel2(rootFileObject); |
| } |
| } |
| rootCollector.add(rootFile); |
| if (result == null) { |
| final FileObject moduleInfo = rootFileObject.getFileObject(MODULE_INFO_JAVA); |
| if (moduleInfo != null) { |
| result = moduleInfo; |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| @CheckForNull |
| private static String getModuleName(@NonNull final FileObject moduleInfo) { |
| try { |
| final String[] res = new String[1]; |
| final JavaSource src = JavaSource.forFileObject(moduleInfo); |
| if (src != null) { |
| src.runUserActionTask((cc) -> { |
| cc.toPhase(JavaSource.Phase.PARSED); |
| final CompilationUnitTree cu = cc.getCompilationUnit(); |
| for (Tree decl : cu.getTypeDecls()) { |
| if (decl.getKind().name().equals("MODULE")) { |
| res[0] = ((ModuleTree)decl).getName().toString(); |
| break; |
| } |
| } |
| }, true); |
| } |
| return res[0]; |
| } catch (IOException ioe) { |
| LOG.log( |
| Level.WARNING, |
| "Cannot read module declaration in: {0} due to: {1}", //NOI18N |
| new Object[]{ |
| FileUtil.getFileDisplayName(moduleInfo), |
| ioe.getMessage() |
| }); |
| return null; |
| } |
| } |
| |
| private static enum TestMode { |
| /** |
| * Tests for pre JDK9 sources. |
| */ |
| LEGACY { |
| @Override |
| List<String> createArguments( |
| @NullAllowed final FileObject srcModuleInfo, |
| @NullAllowed final FileObject testModuleInfo) { |
| return Collections.emptyList(); |
| } |
| }, |
| /** |
| * Tests for an unnamed module. |
| */ |
| UNNAMED { |
| @Override |
| List<String> createArguments( |
| @NullAllowed final FileObject srcModuleInfo, |
| @NullAllowed final FileObject testModuleInfo) { |
| return Collections.emptyList(); |
| } |
| }, |
| /** |
| * Tests inlined into names module in sources. |
| */ |
| INLINED { |
| @Override |
| List<String> createArguments( |
| @NullAllowed final FileObject srcModuleInfo, |
| @NullAllowed final FileObject testModuleInfo) { |
| final String moduleName = getModuleName(srcModuleInfo); |
| if (moduleName == null) { |
| return Collections.emptyList(); |
| } |
| final List<String> result = Arrays.asList( |
| String.format("-XD-Xmodule:%s", moduleName), //NOI18N |
| "--add-reads", //NOI18N |
| String.format("%s=ALL-UNNAMED", moduleName)); //NOI18N |
| return Collections.unmodifiableList(result); |
| } |
| }, |
| /** |
| * Tests have its own module. |
| */ |
| MODULE { |
| @Override |
| List<String> createArguments( |
| @NullAllowed final FileObject srcModuleInfo, |
| @NullAllowed final FileObject testModuleInfo) { |
| final String moduleName = getModuleName(testModuleInfo); |
| if (moduleName == null) { |
| return Collections.emptyList(); |
| } |
| final List<String> result = Arrays.asList( |
| "--add-reads", //NOI18N |
| String.format("%s=ALL-UNNAMED", moduleName)); //NOI18N |
| return Collections.unmodifiableList(result); |
| } |
| }; |
| |
| @NonNull |
| abstract List<String> createArguments( |
| @NullAllowed final FileObject srcModuleInfo, |
| @NullAllowed final FileObject testModuleInfo); |
| } |
| } |
| } |