Software development of explosion! -夢の破片(カケラ)たちの日々-

ソフトウェア開発を中心としたコンピューター関連のネタを扱ったブログです

Software development is passion and explosion!

出来た−!!

http://www.tsuyukimakoto.com/blog/2007/11/08/java-package/のサイトを参考に、Javaで指定したパッケージ配下のクラスの一覧を取得するプログラムを書きました。
コテコテに機能を増やしてしまったのと、勉強がてらStateパターンっぽいものを使って見たりとか・・・
いや、Stateパターンになっているか怪しいうえに、なっていたとして、適用方法も怪しかったりする・・・(実はStateパターンじゃなくてFactoryもどきになっているなぁ(汗))
あと、コメント・・・疲れたので殆ど書いていません。
いや、この長さを1つのJavaファイルに書くってアホだな、俺(でも、クラス分割的にはこれ以上はpublicなクラスを増やしたくないんだけどねぇ・・・C#なら出来たのかな?)

ちなみに、↓のクラスをjarなんかのアーカイブに入れた場合と、Google App Engineのプロダクト環境では動作確認は全くしていません。
とりあえず、AppEngine SDKの開発サーバーでは動くことは確認しています。あと、多分、Jettyでなら動くと思います。
(サブパッケージを追うところはチェックしてないけど、Class.getResource("/").getPath()でパスを取って・・・の処理はJettyのためだったりする)
Tomcatとかでも動くのかな?
あと、MacOSの上だとか、Linuxの上だとか、Windows 7 x64以外の環境では動かしていないから、パス区切り文字とかのあたりでコケる可能性もあるけど、
そもそも、このソースを使う場合には own your risk(自己責任)でお願いします。
(って言うか、いつもの通り、Apache License 2.0 だし、勉強を兼ねて作ったものを公開しているので文句を言われても困るけど)

/*
 * Copyright 2010 PoaD.
 * 
 * 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 tv.dyndns.poad.utils;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * 指定したパッケージ配下のクラスの一覧を取得する機能を提供します。
 * 
 * @author PoaD
 */
public class Package {
    protected static String DIR_SEPARATOR = File.separator;
    protected static String PATH_SEPARATOR = System
        .getProperty("path.separator");
    protected static String PACKAGE_SEPARATOR = ".";

    public static final boolean WITH_OUT_SUBPACKAGE = false;
    public static final boolean WITH_IN_SUBPACKAGE = true;

    public static final boolean WITH_OUT_ANONYMOUS_CLASS = false;
    public static final boolean WITH_IN_ANONYMOUS_CLASS = true;

    protected String packageName;
    private boolean withInSubPackage;
    private boolean withInAnonymousClass;

    private static enum State {
        ARCHIVE, FILESYSTEM
    }

    /**
     * State
     * パターン(になってるかな?)で、jar、zipか、fsかの状態に応じて、クラス一覧のLookupper実装クラスを切り替えるための状態テーブル
     */
    private static final HashMap<State, Lookupper> state =
        new HashMap<State, Lookupper>() {
            /**
         * 
         */
            private static final long serialVersionUID = 1L;

            {
                this.put(State.ARCHIVE, new ArchiveClassLookupper());
                this.put(State.FILESYSTEM, new FileSystemClassLookupper());
            }
        };

    /**
     * @param packageName
     *            パッケージ名
     */
    public Package(final String packageName) {
        this(packageName, WITH_OUT_SUBPACKAGE);
    }

    /**
     * 
     * @param packageName
     *            パッケージ名
     * @param withInSubPackage
     *            true の場合、サブパッケージ配下のクラスを含んだ形でクラス一覧を検出するようになります。
     */
    public Package(final String packageName, final boolean withInSubPackage) {
        this(packageName, withInSubPackage, WITH_OUT_ANONYMOUS_CLASS);
    }

    /**
     * 
     * @param packageName
     *            パッケージ名
     * @param withInSubPackage
     *            true の場合、サブパッケージも含んだ形でクラス一覧を検出するようになります。
     * @param withInAnonymousClass
     *            true の場合、$がFQDNに含まれるクラスを含んだ形でクラス一覧を検出するようになります。
     */
    public Package(final String packageName, final boolean withInSubPackage,
            final boolean withInAnonymousClass) {
        this.packageName = packageName;
        this.setWithInSubPackage(withInSubPackage);
        this.setWithInAnonymousClass(withInAnonymousClass);
    }

    /**
     * 
     * @return このオブジェクトが現すパッケージ名をキーに持ち、値に取得したクラス一覧を持つMapオブジェクト
     * @throws IOException
     *             クラスの一覧の取得に失敗した場合
     * @throws ClassNotFoundException
     *             クラスの一覧の取得に失敗した場合
     */
    public HashMap<String, List<Class<?>>> getClasses() throws IOException,
            ClassNotFoundException {
        return Package.getClasses(
            new String[] { this.packageName },
            this.withInSubPackage,
            this.withInAnonymousClass);
    }

    /**
     * 
     * @param packageNames
     *            取得するパッケージ名の一覧
     * @return 
     *         キーとして、パラメーターとして渡されたパッケージ名を持ち、キーのパッケージ名を基に取得されたクラス一覧を値として持つMapオブジェクト
     * @throws IOException
     *             クラスの一覧の取得に失敗した場合
     * @throws ClassNotFoundException
     *             クラスの一覧の取得に失敗した場合
     */
    public static HashMap<String, List<Class<?>>> getClasses(
            final String[] packageNames) throws IOException,
            ClassNotFoundException {
        return getClasses(packageNames, WITH_OUT_SUBPACKAGE);
    }

    /**
     * 
     * @param packageNames
     *            取得するパッケージ名の一覧
     * @param withInSubPackage
     *            true の場合、サブパッケージも含んだ形でクラス一覧を検出するようになります。
     * @return 
     *         キーとして、パラメーターとして渡されたパッケージ名を持ち、キーのパッケージ名を基に取得されたクラス一覧を値として持つMapオブジェクト
     * @throws IOException
     *             クラスの一覧の取得に失敗した場合
     * @throws ClassNotFoundException
     *             クラスの一覧の取得に失敗した場合
     */
    public static HashMap<String, List<Class<?>>> getClasses(
            final String[] packageNames, final boolean withInSubPackage)
            throws IOException, ClassNotFoundException {
        return getClasses(
            packageNames,
            withInSubPackage,
            WITH_OUT_ANONYMOUS_CLASS);
    }

    /**
     * 
     * @param packageNames
     *            取得するパッケージ名の一覧
     * @param withInSubPackage
     *            true の場合、サブパッケージも含んだ形でクラス一覧を検出するようになります。
     * @param withInAnonymousClass
     *            true の場合、$がFQDNに含まれるクラスを含んだ形でクラス一覧を検出するようになります。
     * @return 
     *         キーとして、パラメーターとして渡されたパッケージ名を持ち、キーのパッケージ名を基に取得されたクラス一覧を値として持つMapオブジェクト
     * @throws IOException
     *             クラスの一覧の取得に失敗した場合
     * @throws ClassNotFoundException
     *             クラスの一覧の取得に失敗した場合
     */
    public static HashMap<String, List<Class<?>>> getClasses(
            final String[] packageNames, final boolean withInSubPackage,
            final boolean withInAnonymousClass) throws IOException,
            ClassNotFoundException {
        HashMap<String, List<Class<?>>> map =
            new LinkedHashMap<String, List<Class<?>>>();
        for (String app : packageNames) {
            map.put(app, new ArrayList<Class<?>>());
        }
        ArrayList<String> pathList = getPathList();
        for (String path : pathList) {
            Lookupper lookupper = null;
            if (path.endsWith(".jar") || path.endsWith(".zip")) {
                lookupper = state.get(State.ARCHIVE);
            } else {
                lookupper = state.get(State.FILESYSTEM);
            }
            lookupper.lookup(
                path,
                packageNames,
                map,
                withInSubPackage,
                withInAnonymousClass);
        }
        return map;
    }

    /**
     * 
     * @return 
     *         システムプロパティの"java.class.path"で取得したクラスパスおよび、このクラスの所属するパッケージの最上位(デフォルト
     *         )パッケージが現すディレクトリのパスの一覧
     * @throws UnsupportedEncodingException
     *             取得したこのクラスの所属するパッケージの最上位(デフォルト)
     *             パッケージが現すディレクトリのパスの文字エンコーディングUTF-8でなかった場合
     */
    protected static ArrayList<String> getPathList()
            throws UnsupportedEncodingException {
        String systemPath = System.getProperty("java.class.path");
        String[] pathes = systemPath.split(PATH_SEPARATOR);
        ArrayList<String> pathList = new ArrayList<String>(pathes.length + 1);
        final String currentRootPath = getCurrentRootPackagePath();
        for (String path : pathes) {
            if (!path.equalsIgnoreCase(currentRootPath)) {
                pathList.add(path);
            }
        }
        pathList.add(currentRootPath);
        return pathList;
    }

    /**
     * 
     * @return このクラスの所属するパッケージの最上位(デフォルト)パッケージが現すディレクトリのパス
     * @throws UnsupportedEncodingException
     *             取得したこのクラスの所属するパッケージの最上位(デフォルト)
     *             パッケージが現すディレクトリのパスの文字エンコーディングUTF-8でなかった場合
     */
    protected static String getCurrentRootPackagePath()
            throws UnsupportedEncodingException {
        String path = Package.class.getResource("/").getPath();
        path = URLDecoder.decode(path, "UTF-8");
        if (OSChecker.isWindows() && path.matches("^/[a-zA-Z]:/.*")) {
            path = path.substring(1, path.length());
        }
        return path;
    }

    /**
     * @param withInSubPackage
     *            セットする withInSubPackage
     */
    public void setWithInSubPackage(final boolean withInSubPackage) {
        this.withInSubPackage = withInSubPackage;
    }

    /**
     * @return withInSubPackage
     */
    public boolean getWithInSubPackage() {
        return this.withInSubPackage;
    }

    /**
     * @param withInAnonymousClass
     *            セットする withInAnonymousClass
     */
    public void setWithInAnonymousClass(final boolean withInAnonymousClass) {
        this.withInAnonymousClass = withInAnonymousClass;
    }

    /**
     * @return withInAnonymousClass
     */
    public boolean isWithInAnonymousClass() {
        return this.withInAnonymousClass;
    }
}

/**
 * 
 * @author PoaD
 * 
 */
interface Lookupper {

    /**
     * Lookup.
     * 
     * @param path
     *            the path
     * @param packageNames
     *            the package names
     * @param out
     *            the out
     * @param withInSubPackage
     *            the with in sub package
     * @param withInAnonymousClass
     *            the with in anonymous class
     * @throws FileNotFoundException
     *             the file not found exception
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     * @throws ClassNotFoundException
     *             the class not found exception
     */
    void lookup(final String path, final String[] packageNames,
            final HashMap<String, List<Class<?>>> out,
            final boolean withInSubPackage, final boolean withInAnonymousClass)
            throws FileNotFoundException, IOException, ClassNotFoundException;
}

/**
 * 
 * @author PoaD
 * 
 */
class ArchiveClassLookupper implements Lookupper {

    /**
     * @author PoaD
     * 
     */
    private interface withInSubPackageChecker {

        /**
         * Check.
         * 
         * @param s1
         *            the s1
         * @param s2
         *            the s2
         * @return true, if successful
         */
        boolean check(final String s1, final String s2);
    }

    /**
     * @author PoaD
     * 
     */
    private interface withInAnonymousChecker {

        /**
         * Check.
         * 
         * @param className
         *            the class name
         * @return true, if successful
         */
        boolean check(final String className);
    }

    /**
     * 
     */
    private final HashMap<Boolean, withInSubPackageChecker> pkgState =
        new HashMap<Boolean, withInSubPackageChecker>() {
            /**
         * 
         */
            private static final long serialVersionUID = 1L;

            {
                this.put(Boolean.TRUE, new withInSubPackageChecker() {
                    /*
                     * (非 Javadoc)
                     * 
                     * @see tv.dyndns.poad.utils.ArchiveClassLookupper.
                     * withInSubPackageChecker#check(java.lang.String,
                     * java.lang.String)
                     */
                    public boolean check(final String s1, final String s2) {
                        return s1.endsWith(".class") && s1.startsWith(s2);
                    }
                });
                this.put(Boolean.FALSE, new withInSubPackageChecker() {

                    /*
                     * (非 Javadoc)
                     * 
                     * @see tv.dyndns.poad.utils.ArchiveClassLookupper.
                     * withInSubPackageChecker#check(java.lang.String,
                     * java.lang.String)
                     */
                    public boolean check(final String s1, final String s2) {
                        String s1buffer = s1;
                        if (!s1.endsWith(".class")) {
                            return false;
                        }
                        s1buffer = s1.replace(".class", "");
                        s1buffer =
                            s1buffer.substring(0, s1buffer.lastIndexOf("."));

                        return s1buffer.equals(s2);
                    }
                });
            }
        };

    private final HashMap<Boolean, withInAnonymousChecker> clsState =
        new HashMap<Boolean, withInAnonymousChecker>() {
            /**
             * 
             */
            private static final long serialVersionUID = 1L;

            {
                this.put(Boolean.TRUE, new withInAnonymousChecker() {
                    /*
                     * (非 Javadoc)
                     * 
                     * @see tv.dyndns.poad.utils.ArchiveClassLookupper.
                     * withInAnonymousChecker#check(java.lang.String)
                     */
                    public boolean check(final String s1) {
                        return true;
                    }
                });
                this.put(Boolean.FALSE, new withInAnonymousChecker() {

                    /*
                     * (非 Javadoc)
                     * 
                     * @see tv.dyndns.poad.utils.ArchiveClassLookupper.
                     * withInAnonymousChecker#check(java.lang.String)
                     */
                    public boolean check(final String s1) {
                        return !s1.contains("$");
                    }
                });
            }
        };

    /*
     * (非 Javadoc)
     * 
     * @see tv.dyndns.poad.utils.Lookupper#lookup(java.lang.String,
     * java.lang.String[], java.util.HashMap, boolean, boolean)
     */
    public void lookup(final String path, final String[] packageNames,
            final HashMap<String, List<Class<?>>> out,
            final boolean withInSubPackage, final boolean withInAnonymousClass)
            throws FileNotFoundException, IOException, ClassNotFoundException {
        JarInputStream jis = null;
        try {
            jis =
                new JarInputStream(new BufferedInputStream(new FileInputStream(
                    path)));
            JarEntry entry;
            while ((entry = jis.getNextJarEntry()) != null) {
                String name =
                    entry
                        .getName()
                        .replace(
                            Package.DIR_SEPARATOR,
                            Package.PACKAGE_SEPARATOR)
                        .replace("/", Package.PACKAGE_SEPARATOR);
                if (name.endsWith(Package.PACKAGE_SEPARATOR)) {
                    name = name.substring(0, name.length() - 1);
                }
                for (String app : packageNames) {
                    if (this.pkgState.get(withInSubPackage).check(name, app)
                        && this.clsState.get(withInAnonymousClass).check(name)) {
                        out.get(app).add(
                            Class.forName(name.replace(".class", "")));
                    }
                }
            }
        } finally {
            if (jis != null) {
                jis.close();
            }
        }
    }
}

/**
 * 
 * @author PoaD
 * 
 */
class FileSystemClassLookupper implements Lookupper {

    /**
     * The Interface withInSubPackageBrancher.
     */
    private interface withInSubPackageBrancher {

        /**
         * Branch.
         * 
         * @param directory
         *            the directory
         * @param filter
         *            the filter
         * @param path
         *            the path
         * @param result
         *            the result
         * @throws FileNotFoundException
         *             the file not found exception
         * @throws IOException
         *             Signals that an I/O exception has occurred.
         * @throws ClassNotFoundException
         *             the class not found exception
         */
        void branch(final File directory, final FilenameFilter filter,
                final String path, final List<Class<?>> result)
                throws FileNotFoundException, IOException,
                ClassNotFoundException;
    }

    /**
     * A factory for creating FilenameFilter objects.
     */
    private static class FilenameFilterFactory {
        protected static final HashMap<Boolean, FilenameFilter> filters =
            new HashMap<Boolean, FilenameFilter>() {
                /**
                 * 
                 */
                private static final long serialVersionUID = 1L;

                {
                    this.put(Boolean.TRUE, new FilenameFilter() {

                        /*
                         * (非 Javadoc)
                         * 
                         * @see java.io.FilenameFilter#accept(java.io.File,
                         * java.lang.String)
                         */
                        public boolean accept(final File dir, final String name) {
                            return name.toLowerCase().endsWith(".class");
                        }
                    });
                    this.put(Boolean.FALSE, new FilenameFilter() {

                        /*
                         * (非 Javadoc)
                         * 
                         * @see java.io.FilenameFilter#accept(java.io.File,
                         * java.lang.String)
                         */
                        public boolean accept(final File dir, final String name) {
                            return !name.contains("$")
                                && name.toLowerCase().endsWith(".class");
                        }
                    });
                }
            };

        /**
         * Gets the filter.
         * 
         * @param withInAnonymousClass
         *            the with in anonymous class
         * @return the filter
         */
        public static FilenameFilter getFilter(
                final boolean withInAnonymousClass) {
            return filters.get(withInAnonymousClass);
        }
    }

    private final HashMap<Boolean, withInSubPackageBrancher> brancher =
        new HashMap<Boolean, withInSubPackageBrancher>() {
            /**
         * 
         */
            private static final long serialVersionUID = 1L;

            {
                this.put(Boolean.TRUE, new withInSubPackageBrancher() {

                    /**
                     * Lookup sub directories.
                     * 
                     * @param directory
                     *            the directory
                     * @return the file[]
                     * @throws FileNotFoundException
                     *             the file not found exception
                     * @throws IOException
                     *             Signals that an I/O exception has occurred.
                     * @throws ClassNotFoundException
                     *             the class not found exception
                     */
                    private File[] lookupSubDirectories(final File directory)
                            throws FileNotFoundException, IOException,
                            ClassNotFoundException {
                        return directory.listFiles(new FileFilter() {

                            public boolean accept(final File file) {
                                return file.isDirectory();
                            }
                        });

                    }

                    /*
                     * (非 Javadoc)
                     * 
                     * @see tv.dyndns.poad.utils.FileSystemClassLookupper.
                     * withInSubPackageBrancher#branch(java.io.File,
                     * java.io.FilenameFilter, java.lang.String, java.util.List)
                     */
                    public void branch(final File directory,
                            final FilenameFilter filter, final String path,
                            final List<Class<?>> result)
                            throws FileNotFoundException, IOException,
                            ClassNotFoundException {
                        if (directory != null) {
                            File[] subdirs =
                                this.lookupSubDirectories(directory);
                            for (File subdir : subdirs) {
                                FileSystemClassLookupper.this
                                    .lookupFromCurrentDirectory(
                                        subdir,
                                        filter,
                                        path,
                                        result);
                            }
                        }

                    }
                });

                this.put(Boolean.FALSE, new withInSubPackageBrancher() {

                    /*
                     * (非 Javadoc)
                     * 
                     * @see tv.dyndns.poad.utils.FileSystemClassLookupper.
                     * withInSubPackageBrancher#branch(java.io.File,
                     * java.io.FilenameFilter, java.lang.String, java.util.List)
                     */
                    public void branch(final File directory,
                            final FilenameFilter filter, final String path,
                            final List<Class<?>> result)
                            throws FileNotFoundException, IOException,
                            ClassNotFoundException {
                        // 何もしない
                    }
                });

            }
        };

    /*
     * (非 Javadoc)
     * 
     * @see tv.dyndns.poad.utils.Lookupper#lookup(java.lang.String,
     * java.lang.String[], java.util.HashMap, boolean, boolean)
     */
    public void lookup(final String path, final String[] packageNames,
            final HashMap<String, List<Class<?>>> out,
            final boolean withInSubPackage, final boolean withInAnonymousClass)
            throws FileNotFoundException, IOException, ClassNotFoundException {
        StringBuffer dir = null;
        for (String pkg : packageNames) {
            dir = new StringBuffer(path);
            dir.append(Package.DIR_SEPARATOR);
            dir.append(pkg.replace(
                Package.PACKAGE_SEPARATOR,
                Package.DIR_SEPARATOR));
            try {
                File directory = new File(dir.toString());
                List<Class<?>> result = out.get(pkg);
                FilenameFilter filter =
                    FilenameFilterFactory.getFilter(withInAnonymousClass);
                this
                    .lookupFromCurrentDirectory(directory, filter, path, result);
                this.brancher.get(withInSubPackage).branch(
                    directory,
                    filter,
                    path,
                    result);
            } catch (Exception e) {
            }
        }
    }

    /**
     * Lookup from current directory.
     * 
     * @param directory
     *            the directory
     * @param filter
     *            the filter
     * @param path
     *            the path
     * @param result
     *            the result
     * @throws FileNotFoundException
     *             the file not found exception
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     * @throws ClassNotFoundException
     *             the class not found exception
     */
    private void lookupFromCurrentDirectory(final File directory,
            final FilenameFilter filter, final String path,
            final List<Class<?>> result) throws FileNotFoundException,
            IOException, ClassNotFoundException {
        File[] classes = directory.listFiles(filter);
        if (classes != null) {
            StringBuffer replace = new StringBuffer(path);
            replace.append(Package.DIR_SEPARATOR);
            for (File clazz : classes) {
                final String className =
                    clazz
                        .getPath()
                        .replace(replace.toString(), "")
                        .replace(
                            Package.DIR_SEPARATOR,
                            Package.PACKAGE_SEPARATOR)
                        .replace(".class", "");
                result.add(Class.forName(className));
            }
        }

    }

}