たぷつきません

おなかがでてきた。もうたぷついてるやん。

日本語化ライブラリを含めたRCP製品エクスポートが失敗する問題

 pleiadesではまれにキャッシュが壊れてconfigurationを削除しないとeclipseやrcpが立ち上がらなくなってしまう。それをユーザーに伝えるFAQを用意することを考えたら、日本語化パックを使うほうが全然良いだろうと思って差し替えてみたところ、.productのEclipse製品エクスポートで失敗してしまう。
 原因は、日本語化パックに埋め込まれたMANIFESTに不具合があるため。以下のソース2つを取り込んだコンソールアプリを実行すれば、.productのエクスポートで、日本語化パックも含められるようになる。もちろんマルチプラットフォームエクスポートも。
 com.gluegent.tools.NLPackagePatch がmainを含む本体で、com.gluegent.tools.ConsoleApplicationがパラメータをフィールドに自動的にバインドしてくれる支援クラス。ちょっと凝って便利に作ったのでコンソールプログラムを作ろうとしたら使ってくれると幸いです。NLPackagePatchがそのまま利用サンプルになってますし。

com/gluegent/tools/NLPackagePatch.java
/*
 * Copyright 2009 the Seasar Foundation and the Others.
 *
 * 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.gluegent.tools;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

/**
 * SourceForge.JP プロジェクト » nttdatagroup-oss-square » Wiki » blanco_Framework/nlpack.eclipse.ganymede
 *  (http://sourceforge.jp/projects/nttdatagroup-oss-square/wiki/blanco_Framework/nlpack.eclipse.ganymede)
 * で公開されている言語パックでは、Eclipse RCPの製品エクスポートができない。
 * これは、2点の問題がある。
 * まず1つが、fragment-hostとして依存している先のプラグインがPlatform依存のものの場合で、それがPlatform-Filterが掛かっている場合に
 * 起こる。マルチプラットフォームエクスポートで対象プラットフォームでないのにも拘わらず全プラットフォームで選択されてしまうためである。
 * もう1つが、nlはfragmentなのだが、pluginではなく別のプラットフォーム別のfragmentにぶらさがろうとする意味不明な設定があるためである。
 * 
 * この2点は同時に1つのnlフラグメントの中で起きているので以下の対応を行うことで解決させる。
 * 1つ目は、依存しようとしているプラットフォーム別のフラグメント(プラグインにしなければならず、正しくないのだが)のMETA-INFに、Platform-Filterの
 * 定義があれば、コピーする。
 * もう1つは、Fragment-Hostを、依存しようとしているプラットフォーム別のフラグメントのFragment-Hostと同一にして、正しくプラグインにぶら下がるように
 * する。
 * 
 * @author KATO Taro (Gluegent, Inc.)
 */
public class NLPackagePatch extends ConsoleApplication {

    public static void main(String[] args) throws Throwable {
        new NLPackagePatch(args).run();
    }
    
    public NLPackagePatch(String[] args) {
        super(args);
    }
    
    @Option(abbr="x", description="標準エラー出力に詳細なログを出力する。")
    private boolean debug = false;
    
    @Option(required=true, description="pluginディレクトリを指定する。\n ex.) \"C:\\Program Files\\eclipse\\plugins\"")
    private File pluginDir;
    
    
    public static final String REGEX_JAR = "_[0-9]+\\.[0-9]+\\.[0-9]+(\\..+)?\\.jar$";
    public static final String REGEX_NL_JAR = ".+\\.nl_ja" + REGEX_JAR;
    
    public void execute() throws Exception {
        if (!pluginDir.isDirectory()) {
            throw new IllegalArgumentException(pluginDir + "は存在するディレクトリではありません。");
        }

        File backupFolder = new File(pluginDir.getParentFile(), "backup_plugins");
        backupFolder.mkdirs();
        
        for (File nl_jar: collectFile(pluginDir, REGEX_NL_JAR)) {
            StringBuilder sb = new StringBuilder(nl_jar.getName() + " ... ");
            JarFile jf = new JarFile(nl_jar);
            Manifest manifest = jf.getManifest();
            String fragmentHost = (String)manifest.getMainAttributes().getValue("Fragment-Host");
            if (fragmentHost == null) {
                if (debug) {
                    sb.append(" skip. [Fragment-Hostがない]");
                    System.err.println(sb);
                }
                continue;
            }
            String platformFilter = (String)manifest.getMainAttributes().getValue("Eclipse-PlatformFilter");
            if (platformFilter != null) {
                sb.append(" skip. [すでに Eclipse-PlatformFilter が記述されている]");
                System.out.println(sb);
                continue;
            }
            
            int delimiterIndex = fragmentHost.indexOf(';');
            if (delimiterIndex > 0) {
                fragmentHost = fragmentHost.substring(0, delimiterIndex);
            }
            
            String warningComment = "";
            File[] fragmentHosts = collectFile(pluginDir, fragmentHost + REGEX_JAR);
            if (fragmentHosts.length == 0) {
                if (debug) {
                    sb.append(" skip. [fragmentHostのpluginファイルが存在しない(" + fragmentHost + ")]");
                    System.err.println(sb);
                }
                continue;
            } else if (fragmentHosts.length > 1) {
                if (debug) {
                    warningComment +=
                        "\n    warning: fragmentHostのpluginファイルが複数存在するので最後に見つかったものを選択しました。 - " + fragmentHosts[fragmentHosts.length-1];
                }
            }
            JarFile hf = new JarFile(fragmentHosts[fragmentHosts.length-1]);
            Manifest hostManifest = hf.getManifest();
            
            fragmentHost = hostManifest.getMainAttributes().getValue("Fragment-Host");
            if (fragmentHost == null) {
                if (debug) {
                    sb.append(" skip. [fragmentにぶら下がっている訳ではない]");
                    sb.append(warningComment);
                    System.err.println(sb);
                }
                continue;
            }
            platformFilter = hostManifest.getMainAttributes().getValue("Eclipse-PlatformFilter");
            if (platformFilter == null) {
                if (debug) {
                    sb.append(" skip. [fragmentHostがプラットフォーム固有のプラグインではない]");
                    sb.append(warningComment);
                    System.err.println(sb);
                }
                continue;
            }
            
            File outFile = new File(nl_jar.getAbsolutePath() + ".tmp");
            manifest.getMainAttributes().putValue("Eclipse-PlatformFilter", platformFilter);
            manifest.getMainAttributes().putValue("Fragment-Host", fragmentHost);
            
            FileOutputStream fo = new FileOutputStream(outFile);
            try {
                JarOutputStream jos = new JarOutputStream(fo, manifest);
                try {
                    FileInputStream fi = new FileInputStream(nl_jar);
                    try {
                        JarInputStream jis = new JarInputStream(fi);
                        byte[] buffer = new byte[1024];
                        JarEntry je;
                        for (; null != (je = jis.getNextJarEntry()); ) {
                            if (!je.getName().equals("META-INF/MANIFEST.MF")) {
                                jos.putNextEntry(je);
                                try {
                                    if (!je.isDirectory()) {
                                        int readn;
                                        while (0 <= (readn = jis.read(buffer))) {
                                            jos.write(buffer, 0, readn);
                                        }
                                    }
                                } finally {
                                    jos.closeEntry();
                                }
                            }
                        }
                        jos.finish();
                    } finally {
                        fi.close();
                    }
                } finally {
                    jos.close();
                }
            } finally {
                fo.close();
            }
            
            File backupFile = new File(backupFolder, nl_jar.getName());
            if (backupFile.exists()) {
                if (!backupFile.delete()) {
                    sb.append(" success. but cannot replaced. - !!古いオリジナルファイル("+backupFile.getName()+")が削除できないため、作成したファイルは" + outFile.getName() + " のままです。これを.tmpを外して手動で置換してください。" + warningComment);
                    System.err.println(sb);
                    continue;
                }
            }
            
            jf.close();
            
            if (!nl_jar.renameTo(backupFile)) {
                sb.append(" failure. - 以前のファイルを"+backupFolder.getCanonicalPath()+"に移動できません。作成したファイルは" + outFile.getName() + " のままです。これを.tmpを外して手動で置換してください。" + warningComment);
                System.err.println(sb);
                continue;
            }
            if (!outFile.renameTo(nl_jar)) {
                sb.append(" failure. - 作成したファイル(" + outFile.getName() + ")を"+nl_jar.getName()+"に置換できません。ロック状態を確認した上で手動で置換してください。" + warningComment);
                System.err.println(sb);
                continue;
            }
            sb.append(" success! - 以前のファイルは"+backupFolder.getCanonicalPath()+"に移動しました。" + warningComment);
            System.out.println(sb);
        }
        System.out.println();
        System.out.println("done.");
    }
    
    public static File[] collectFile(File dir, String regexp) {
        return dir.listFiles(new RegexpFilenameFilter(regexp, true));
    }
    
    public static class RegexpFilenameFilter implements FilenameFilter {
        private String regexpString;
        private Boolean fileOnly;
        
        public RegexpFilenameFilter(String regexpString, Boolean fileOnly) {
            this.regexpString = regexpString;
            this.fileOnly = fileOnly;
        }
        
        private boolean check(File dir, String name) {
            if (fileOnly != null) {
                File child = (new File(dir, name));
                if (!child.exists()) {
                    return false;
                }
                if (fileOnly == true) {
                    return child.isFile();
                } else {
                    return child.isDirectory();
                }
            }
            return true;
        }
        
        public boolean accept(File dir, String name) {
            return name.matches(regexpString) && check(dir, name);
        }
    }
}
com/gluegent/tools/ConsoleApplication.java
/*
 * Copyright 2009 the Seasar Foundation and the Others.
 *
 * 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.gluegent.tools;

import java.io.File;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * パラメータ取り込みを自動化できるコンソールアプリケーション支援クラス。
 * 
 * @author KATO Taro (Gluegent, Inc.)
 */
public abstract class ConsoleApplication implements Runnable {

    private static Map<Class<Object>, ParameterConverter<Object>> converters =
        new HashMap<Class<Object>, ParameterConverter<Object>>();
    
    @SuppressWarnings("unchecked")
    public static <T> void registerParameteConverter(Class<T> type, ParameterConverter<T> converter) {
        converters.put((Class<Object>)type, (ParameterConverter<Object>)converter);
    }
    
    static {
        registerParameteConverter(String.class, new ParameterConverter<String>() {
            public String convert(String arg) {
                return arg;
            }
        });
        registerParameteConverter(URI.class, new ParameterConverter<URI>() {
            public URI convert(String arg) throws URISyntaxException {
                return new URI(arg);
            }
        });
        registerParameteConverter(URL.class, new ParameterConverter<URL>() {
            public URL convert(String arg) throws MalformedURLException {
                return new URL(arg);
            }
        });
        registerParameteConverter(File.class, new ParameterConverter<File>() {
            public File convert(String arg) throws Exception {
                return new File(arg);
            }
        });
        registerParameteConverter(Integer.class, new ParameterConverter<Integer>() {
            public Integer convert(String arg) throws Exception {
                return Integer.parseInt(arg);
            }
        });
    }
    
    public ConsoleApplication(String[] args) {
        this.args = args;
    }
    
    public void run() {
        try {
            init();
            execute();
        } catch(IllegalArgumentException e) {
            System.err.println(e.getMessage());
            printUsage();
        } catch(Exception e) {
            e.printStackTrace();
            printUsage();
        }
    }

    protected String[] args;
    
    protected void init() {
        extractOptions();
    }
    
    private boolean hasOption(Field field) {
        return getOption(field) != null;
    }
    
    private Option getOption(Field field) {
        return field.getAnnotation(Option.class);
    }
    
    private List<Field> getOptionFields() {
        List<Field> fields = new ArrayList<Field>();
        for (Field field: getClass().getDeclaredFields()) {
            if (hasOption(field)) {
                field.setAccessible(true);
                fields.add(field);
            }
        }
        return fields;
    }
    
    private String toOptionName(Field field) {
        if (getOptionFields().contains(field)) {
            return "--" + uncapitalize(field.getName());
        }
        return null;
    }
    private Field toOptionField(String name, boolean fromAbbr) {
        for (Field field: getOptionFields()) {
            if (fromAbbr) {
                if (getOption(field).abbr().equals(name)) {
                    return field;
                }
            } else {
                if (field.getName().equals(name)) {
                    return field;
                }
            }
        }
        return null;
    }
    
    private boolean isSwitchType(Field field) {
        return field.getType() == boolean.class
                    || field.getType() == Boolean.class;
    }
    
    private Field[] getDefaultOptionFields(boolean unassigns) {
        List<Field> result = new ArrayList<Field>();
        for (Field field: getOptionFields()) {
            if (getOption(field).required() && !isSwitchType(field)) {
                if (unassigns && assignedFields.contains(field)) {
                    continue;
                }
                result.add(field);
            }
        }
        return result.toArray(new Field[result.size()]);
    }
    
    private Set<Field> assignedFields = new HashSet<Field>();
    
    private Field assign(Field field) {
        Field result = null;
        try {
            if (field.getType() == Boolean.class) {
                field.set(this, Boolean.TRUE);
                assignedFields.add(field);
            } else if (field.getType() == boolean.class) {
                field.set(this, true);
                assignedFields.add(field);
            } else if (converters.keySet().contains(field.getType())) {
                result = field;
            } else {
                throw new IllegalStateException("サポートしていないパラメータ型 - " + field.getType().getName());
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
        return result;
    }
    
    private void assign(Field field, String arg) {
        try {
            ParameterConverter<?> converter = converters.get(field.getType());
            if (converter == null) {
                throw new IllegalStateException("サポートしていないパラメータ型 - " + field.getType().getName());
            }
            field.set(this, converter.convert(arg));
            assignedFields.add(field);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
        
    }
    
    /**
     * オプションの取り出し
     */
    private void extractOptions() {
        
        List<String> others = new ArrayList<String>();
        Field nextGet = null;
        String getArg = null;;
        for (String arg: args) {
            if (nextGet != null) {
                try {
                    assign(nextGet, arg);
                } finally {
                    nextGet = null;
                }
                continue;
            }
            if (arg.charAt(0) == '-') {
                String name;
                Field field;
                if (arg.startsWith("--")) {
                    // フル名称
                    name = capitalize(arg.substring("--".length()));
                    field = toOptionField(name, false);
                } else {
                    // 省略名称
                    name = arg.substring("-".length());
                    field = toOptionField(name, true);
                }
                if (field == null) {
                    throw new IllegalArgumentException("不明なパラメータ : " + arg);
                }
                nextGet = assign(field);
                getArg = arg;
                continue;
            }
            others.add(arg);
        }
        if (nextGet != null) {
            throw new IllegalStateException(getArg + " に続けてパラメータが必要です。");
        }
        Field[] fields = getDefaultOptionFields(true);
        if (others.size() > fields.length) {
            // 一度指定したパラメータは渡せません。(そうだった場合のみ)
            String message = "パラメータが多すぎます。 ";
            if (getDefaultOptionFields(false).length > fields.length) {
                throw new IllegalArgumentException(message);
            }
        } else if (others.size() < fields.length) {
            // パラメータが不足しています。 
            String message = "パラメータが不足しています。 ";
            for (int i = others.size(); i < fields.length; i++) {
                message += getOptionArgumentName(fields[i]) + " ";
            }
            throw new IllegalArgumentException(message);
        } else {
            for (int i = 0; i < fields.length; i++) {
                assign(fields[i], others.get(i));
            }
        }
    }
    
    private String getOptionArgumentName(Field field) {
        Option option = getOption(field);
        if (option != null && option.argmentName().length() > 0) {
            return option.argmentName();
        }
        return field.getName();
    }
    
    public static String capitalize(String from) {
        StringBuilder sb = new StringBuilder();
        boolean nextToUpper = false;
        for (char c: from.toCharArray()) {
            if (c == '-') {
                nextToUpper = true;
            } else {
                if (nextToUpper) {
                    sb.append(Character.toUpperCase(c));
                    nextToUpper = false;
                } else {
                    sb.append(Character.toLowerCase(c));
                }
            }
        }
        return sb.toString();
    }
    
    public static String uncapitalize(String from) {
        StringBuilder sb = new StringBuilder();
        for (char c: from.toCharArray()) {
            if (Character.isUpperCase(c)) {
                sb.append('-');
                sb.append(Character.toLowerCase(c));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }
    
    protected void printUsage() {
        System.out.println(usage());
    }
    
    protected String usage() {
        StringBuilder sb = new StringBuilder();
        StringBuilder sb2 = new StringBuilder();
        sb.append("Usage...: ");
        sb.append(getClass().getSimpleName());
        sb.append(" ");
        for (int i = 0; i < 2; i++) {
            boolean choice = (i == 0)?false:true;
            for (Field field: getOptionFields()) {
                Option option = getOption(field);
                if (option.required() == choice) {
                    sb.append("[");
                    sb2.append("\t");
                    if (option.abbr().equals("")) {
                        sb.append(toOptionName(field));
                    } else {
                        sb.append("-" + option.abbr());
                        sb2.append("-" + option.abbr());
                        sb2.append(", ");
                    }
                    sb2.append(toOptionName(field));
                            
                    if (choice) sb.append("]");
                    if (!isSwitchType(field)) {
                        sb.append(" ");
                        if (option.argmentName().equals("")) {
                            sb.append(field.getName());
                        } else {
                            sb.append(option.argmentName());
                        }
                    }
                    if (!choice) sb.append("]");
                    sb.append(" ");
                    sb2.append("\n\t\t");
                    sb2.append(option.description().replace("\r\n", "\n").replace("\n", "\n\t\t"));
                    sb2.append("\n");
                }
            }
        }
        sb.append("\n");
        return sb.toString() + sb2.toString();
    }
    
    /**
     * 実行処理本体。継承クラスが実装する。
     * @throws Throwable
     */
    protected abstract void execute() throws Exception;

    /**
     * コマンドライン引数文字列を型ごとにバインドするためのコンバーター
     * @author KATO Taro (Gluegent, Inc.)
     * @param <T> フィールドの型
     */
    protected static interface ParameterConverter<T> {
        T convert(String argment) throws Exception;
    }
}

/**
 * コマンドラインパラメータをフィールドにバインドさせるためのアノテーション。
 * @author KATO Taro (Gluegent, Inc.)
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Option {
    /**
     * 短縮オプション名称を返す。オプション名称はフィールド名をアンキャピタライズしたもの。
     * @return 短縮オプション名称。空文字列の場合は無いことを示す。
     */
    String abbr() default "";
    
    /**
     * パラメータの説明を返す。
     * @return パラメータの説明
     */
    String description();
    
    /**
     * パラメータ値を代替する名称を返す。
     * @return パラメータ値を代替する名称
     */
    String argmentName() default "";
    
    /**
     * 必須パラメータか否かを返す。
     * @return true=必須パラメータ false=任意パラメータ
     */
    boolean required() default false;
}

 まあ一番いいのはこんなパッチ当てプログラムがなくても正しく治してもらうことなのですが。
 それと、id:cynipeさん調べでは、Tychoでビルドするためには、日本語化パックのfeaturesを全部削除しないとヌルポで落ちるとのこと。Tychoがfeaturesのパッケージからfeatures.xmlを読みにいこうとするんだけど日本語化パックに含まれるものはfragmentとして依存先のfeatures.xmlをあてにするため自身では持っていないから。ZipEntryがないのにgetInputStream()しようとして落ちる。特にRCPでは日本語化したfeaturesが有効にUI上で機能しているところが特に見当たらなかったので、たしかに削除しちゃっても問題ない。