| /* |
| * 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.spi.java.project.support; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| 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.annotations.common.SuppressWarnings; |
| import org.netbeans.api.queries.VisibilityQuery; |
| import org.netbeans.modules.classfile.ClassFile; |
| import org.netbeans.modules.classfile.ClassName; |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.util.Parameters; |
| |
| /** |
| * Miscellaneous helper utils to detect Javadoc root folder, source root folder or |
| * package of the given java or class file. |
| * |
| * @since org.netbeans.modules.java.project/1 1.20 |
| */ |
| public class JavadocAndSourceRootDetection { |
| |
| private static final int JAVADOC_TRAVERSE_DEEPTH = 7; |
| private static final int SRC_TRAVERSE_DEEPTH = 50; |
| |
| private static final Logger LOG = Logger.getLogger(JavadocAndSourceRootDetection.class.getName()); |
| |
| private JavadocAndSourceRootDetection() { |
| } |
| |
| /** |
| * Finds Javadoc root inside of given folder. |
| * |
| * @param fo base folder to start search in; routine will traverse 5 folders |
| * deep before giving up; cannot be null; must be folder |
| * @return found Javadoc root or null if none found |
| */ |
| public static FileObject findJavadocRoot(FileObject baseFolder) { |
| Parameters.notNull("baseFolder", baseFolder); |
| if (!baseFolder.isFolder()) { |
| throw new IllegalArgumentException("baseFolder must be folder: "+baseFolder); // NOI18N |
| } |
| final Set<FileObject> result = new HashSet<>(); |
| findAllJavadocRoots( |
| baseFolder, |
| result, |
| null, |
| true, |
| 0); |
| assert (result.size() & 0xFFFFFFFE) == 0; |
| final Iterator<FileObject> it = result.iterator(); |
| return it.hasNext() ? |
| it.next(): |
| null; |
| } |
| |
| /** |
| * Finds all javadoc roots under the given base folder. |
| * @param baseFolder the base folder to start search in; routine will traverse 5 folders |
| * @param canceled the canceling support |
| * @return the found javadoc roots |
| * @since 1.56 |
| */ |
| @NonNull |
| public static Set<? extends FileObject> findJavadocRoots( |
| @NonNull final FileObject baseFolder, |
| @NullAllowed final AtomicBoolean canceled) { |
| Parameters.notNull("folder", baseFolder); //NOI18N |
| if (!baseFolder.isFolder()) { |
| throw new IllegalArgumentException ("baseFolder must be folder: " + baseFolder); //NOI18N |
| } |
| final Set<FileObject> result = new TreeSet<>((f1,f2) -> { |
| final String f1p = FileUtil.getRelativePath(baseFolder, f1); |
| final String f2p = FileUtil.getRelativePath(baseFolder, f2); |
| return f1p.compareTo(f2p); |
| }); |
| findAllJavadocRoots( |
| baseFolder, |
| result, |
| canceled, |
| false, |
| 0); |
| return Collections.unmodifiableSet(result); |
| } |
| |
| /** |
| * Finds Java sources root inside of given folder. |
| * |
| * @param fo base folder to start search in; routine will traverse subfolders |
| * to find a Java file to detect package root; cannot be null; must be folder |
| * @return found package root of first Java file found or null if none found |
| */ |
| public static FileObject findSourceRoot(FileObject fo) { |
| Parameters.notNull("fo", fo); |
| if (!fo.isFolder()) { |
| throw new IllegalArgumentException("fo must be folder - "+fo); // NOI18N |
| } |
| FileObject root = findJavaSourceFile(fo, 0); |
| if (root != null) { |
| return findPackageRoot(root); |
| } |
| return null; |
| } |
| |
| /** |
| * Finds Java sources roots inside of given folder. |
| * |
| * @param folder to start search in; routine will traverse subfolders |
| * to find a Java file to detect package root; cannot be null; must be folder |
| * @param canceled if set to true the method immediately returns roots it has already found, |
| * may be null |
| * @return {@link Collection} of found package roots |
| * @since 1.31 |
| */ |
| public static Set<? extends FileObject> findSourceRoots(final @NonNull FileObject folder, final @NullAllowed AtomicBoolean canceled) { |
| Parameters.notNull("folder", folder); //NOI18N |
| if (!folder.isValid()) { |
| throw new IllegalArgumentException("Folder: " + FileUtil.getFileDisplayName(folder)+" is not valid."); //NOI18N |
| } |
| if (!folder.isFolder()) { |
| throw new IllegalArgumentException("The parameter: " + FileUtil.getFileDisplayName(folder) + " has to be a directory."); //NOI18N |
| } |
| final Set<FileObject> result = new HashSet<FileObject>(); |
| findAllSourceRoots(folder, result, canceled, 0); |
| return Collections.unmodifiableSet(result); |
| } |
| /** |
| * Returns package root of the given java or class file. |
| * |
| * @param fo either .java or .class file; never null |
| * @return package root of the given file or null if none found |
| */ |
| public static FileObject findPackageRoot(final FileObject fo) { |
| if ("java".equals(fo.getExt())) { // NOI18N |
| return findJavaPackage (fo); |
| } else if ("class".equals(fo.getExt())) { // NOI18N |
| return findClassPackage (fo); |
| } else { |
| throw new IllegalArgumentException("only java or class files accepted "+fo); // NOI18N |
| } |
| } |
| |
| private static FileObject findAllSourceRoots(final FileObject folder, final Collection<? super FileObject> result, |
| final AtomicBoolean canceled, final int depth) { |
| if (depth == SRC_TRAVERSE_DEEPTH) { |
| return null; |
| } |
| if (!VisibilityQuery.getDefault().isVisible(folder)) { |
| return null; |
| } |
| if (isRecursiveSymLink(folder)) { |
| return null; |
| } |
| final FileObject[] children = folder.getChildren(); |
| for (FileObject child : children) { |
| if (canceled != null && canceled.get()) { |
| return null; |
| } else if (child.isData() && "text/x-java".equals(FileUtil.getMIMEType(child, "text/x-java"))) { //NOI18N |
| final FileObject root = findPackageRoot(child); |
| if (root != null) { |
| result.add(root); |
| } |
| return root; |
| } else if (child.isFolder()) { |
| final FileObject upTo = findAllSourceRoots(child, result, canceled, depth+1); |
| if (upTo != null && !upTo.equals(child)) { |
| return upTo; |
| } |
| } |
| } |
| return null; |
| } |
| |
| private static boolean isRecursiveSymLink(@NonNull final FileObject folder) { |
| try { |
| return FileUtil.isRecursiveSymbolicLink(folder); |
| } catch (IOException ioe) { |
| LOG.log( |
| Level.WARNING, |
| "Cannot read link: {0}, reason: {1}", //NOI18N |
| new Object[]{ |
| FileUtil.getFileDisplayName(folder), |
| ioe.getMessage() |
| }); |
| return true; //prevent O(a^n) growth |
| } |
| } |
| |
| private static boolean findAllJavadocRoots( |
| @NonNull final FileObject folder, |
| @NonNull final Collection<? super FileObject> result, |
| @NullAllowed final AtomicBoolean cancel, |
| final boolean singleRoot, |
| final int depth) { |
| final FileObject pkgList = folder.getFileObject("package-list", null); // NOI18N |
| if (pkgList != null) { |
| result.add(folder); |
| return singleRoot; |
| } |
| if (depth == JAVADOC_TRAVERSE_DEEPTH) { |
| return false; |
| } |
| if (cancel != null && cancel.get()) { |
| return true; |
| } |
| for (FileObject file : folder.getChildren()) { |
| if (!file.isFolder()) { |
| continue; |
| } |
| if (findAllJavadocRoots(file, result, cancel, singleRoot, depth+1)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static FileObject findJavaSourceFile(FileObject fo, int level) { |
| if (level == SRC_TRAVERSE_DEEPTH) { |
| return null; |
| } |
| if (!VisibilityQuery.getDefault().isVisible(fo)) { |
| return null; |
| } |
| if (isRecursiveSymLink(fo)) { |
| return null; |
| } |
| // go through files first: |
| for (FileObject fo2 : fo.getChildren()) { |
| if (fo2.isData() && "java".equals(fo2.getExt())) { // NOI18N |
| return fo2; |
| } |
| } |
| // now check sunfolders: |
| for (FileObject fo2 : fo.getChildren()) { |
| if (fo2.isFolder()) { |
| fo2 = findJavaSourceFile(fo2, level+1); |
| if (fo2 != null) { |
| return fo2; |
| } |
| } |
| } |
| return null; |
| } |
| |
| static final Pattern JAVA_FILE, PACKAGE_INFO; |
| static { |
| String whitespace = "(?:(?://[^\n]*\n)|(?:/\\*.*?\\*/)|\\s)"; //NOI18N |
| String javaIdentifier = "(?:\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)"; //NOI18N |
| String packageStatement = "package" + whitespace + "+(" + javaIdentifier + "(?:\\." + javaIdentifier + ")*)" + whitespace + "*;"; //NOI18N |
| JAVA_FILE = Pattern.compile("(?ms)" + whitespace + "*" + packageStatement + ".*", Pattern.MULTILINE | Pattern.DOTALL); //NOI18N |
| // XXX this does not take into account annotations and imports: |
| PACKAGE_INFO = Pattern.compile("(?ms)(?:.*" + whitespace + ")?" + packageStatement + whitespace + "*", Pattern.MULTILINE | Pattern.DOTALL); //NOI18N |
| } |
| |
| @SuppressWarnings({"OS_OPEN_STREAM", "RR_NOT_CHECKED"}) |
| private static FileObject findJavaPackage(FileObject fo) { |
| try { |
| InputStream is = fo.getInputStream(); |
| try { |
| // Try default encoding, probably good enough. |
| Reader r = new BufferedReader(new InputStreamReader(is)); |
| r.mark(2); |
| char[] cbuf = new char[2]; |
| r.read(cbuf, 0, 2); |
| if (cbuf[0] == 255 && cbuf[1] == 254) { // BOM |
| is.close(); |
| is = fo.getInputStream(); |
| r = new BufferedReader(new InputStreamReader(is, "Unicode")); //NOI18N |
| } else { |
| r.reset(); |
| } |
| // TODO: perhaps limit and read just first 100kB and not whole file: |
| StringBuilder b = new StringBuilder((int) fo.getSize()); |
| int read; |
| char[] buf = new char[b.length() + 1]; |
| while ((read = r.read(buf)) != -1) { |
| b.append(buf, 0, read); |
| } |
| Matcher m = (fo.getNameExt().equals("package-info.java") ? PACKAGE_INFO : JAVA_FILE).matcher(b); //NOI18N |
| if (m.matches()) { |
| String pkg = m.group(1); |
| LOG.log(Level.FINE, "Found package declaration {0} in {1}", new Object[] {pkg, fo}); //NOI18N |
| return getPackageRoot(fo, pkg); |
| } else { |
| // XXX probably not a good idea to infer the default package: return f.getParentFile(); |
| return null; |
| } |
| } finally { |
| is.close(); |
| } |
| } catch (IOException x) { |
| LOG.log( |
| Level.INFO, |
| "Cannot read: {0}", //NOI18N |
| FileUtil.getFileDisplayName(fo)); |
| return null; |
| } |
| } |
| |
| @CheckForNull |
| private static FileObject getPackageRoot(@NonNull final FileObject javaOrClassFile, @NonNull final String packageName) { |
| final String[] path = packageName.split("\\."); //NOI18N |
| FileObject pkg = javaOrClassFile.getParent(); |
| for (int i=path.length-1; i>=0; i--) { |
| if (!path[i].equals(pkg.getName())) { |
| return null; |
| } |
| pkg = pkg.getParent(); |
| } |
| return pkg; |
| } |
| |
| |
| /** |
| * Find java package in side .class file. |
| * |
| * @return package or null if not found |
| */ |
| private static FileObject findClassPackage(FileObject file) { |
| try { |
| InputStream in = file.getInputStream(); |
| try { |
| ClassFile cf = new ClassFile(in,false); |
| ClassName cn = cf.getName(); |
| return getPackageRoot(file, cn.getPackage()); |
| } finally { |
| in.close (); |
| } |
| } catch (IOException e) { |
| LOG.log( |
| Level.INFO, |
| "Cannot read: {0}", //NOI18N |
| FileUtil.getFileDisplayName(file)); |
| } |
| return null; |
| } |
| |
| } |