/*
 * 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.apisupport.project;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.event.ChangeListener;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.api.java.platform.JavaPlatformManager;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.modules.apisupport.project.api.Util;
import org.netbeans.modules.apisupport.project.queries.ClassPathProviderImpl;
import org.netbeans.modules.apisupport.project.ui.customizer.ModuleProperties;
import org.netbeans.modules.apisupport.project.ui.customizer.SingleModuleProperties;
import org.netbeans.modules.apisupport.project.universe.DestDirProvider;
import org.netbeans.modules.apisupport.project.universe.ModuleEntry;
import org.netbeans.modules.apisupport.project.universe.ModuleList;
import org.netbeans.modules.apisupport.project.universe.TestModuleDependency;
import org.netbeans.spi.java.project.support.ProjectPlatform;
import org.netbeans.spi.project.support.ant.AntProjectEvent;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.AntProjectListener;
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
import org.netbeans.spi.project.support.ant.PropertyProvider;
import org.netbeans.spi.project.support.ant.PropertyUtils;
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.modules.SpecificationVersion;
import org.openide.util.ChangeSupport;
import org.openide.util.Mutex;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Element;

/**
 * Property evaluator for {@link NbModuleProject}.
 * Has two special behaviors of note:
 * 1. Does not call ModuleList until it really needs to.
 * 2. Is reset upon project.xml changes.
 * @author Jesse Glick, Martin Krauskopf
 */
public final class Evaluator implements PropertyEvaluator, PropertyChangeListener, AntProjectListener {
    
    public static final String CP = "cp";
    public static final String NBJDK_BOOTCLASSPATH = "nbjdk.bootclasspath";
    public static final String NBJDK_BOOTCLASSPATH_MODULAR = "nbjdk.bootclasspath.modular"; //NOI18N
    static final String NBJDK_HOME = "nbjdk.home"; // NOI18N
    public static final String RUN_CP = "run.cp";
    private static final SpecificationVersion JDK9 = new SpecificationVersion("9"); //NOI18N
    
    private final NbModuleProject project;
    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
    
    private PropertyEvaluator delegate;
    private volatile boolean loadedModuleList = false;
    
    /** See issue #69440 for more details. */
    private boolean runInAtomicAction;
    private boolean pendingReset = false;   // issue #173792
    
    private static final Map<Project,Map<String, SoftReference<ModuleEntry>>> cachedProjectModuleEntries = new HashMap<Project,Map<String, SoftReference<ModuleEntry>>>();
    
    private static class TestClasspath {
        
        private final String compile;
        private final String runtime;
        private final String testCompile;
        private final String testRuntime;
        
        public TestClasspath(String compile,String runtime,String testCompile,String testRuntime) {
            this.compile = compile;
            this.runtime = runtime;
            this.testCompile = testCompile;
            this.testRuntime = testRuntime;
        }
        
        public String getCompileClasspath() {
            return compile + ':' + testCompile;
        }
        
        public String getRuntimeClasspath() {
            return runtime + ':' + testRuntime;
        }
        
        private static TestClasspath getOrEmpty(Map<String,TestClasspath> testsCPs, String testtype) {
            TestClasspath tcp = testsCPs.get(testtype);
            if (tcp == null ) {
                // create with empty classpaths
                tcp = new TestClasspath("", "", "", ""); // NOI18N
            }
            return tcp;
        }
    }
    
    @SuppressWarnings("LeakingThisInConstructor")
    Evaluator(NbModuleProject project) {
        this.project = project;
        delegate = createEvaluator(null);
        delegate.addPropertyChangeListener(this);
        project.getHelper().addAntProjectListener(this);
    }
    
    public @Override String getProperty(String prop) {
        PropertyEvaluator eval = delegatingEvaluator(false);
        assert eval != this;
        String v = eval.getProperty(prop);
        if ((v == null && isModuleListDependentProperty(prop)) || isModuleListDependentValue(v)) {
            return delegatingEvaluator(true).getProperty(prop);
        } else {
            return v;
        }
    }
    
    public @Override String evaluate(String text) {
        String v = delegatingEvaluator(false).evaluate(text);
        if (isModuleListDependentValue(v)) {
            return delegatingEvaluator(true).evaluate(text);
        } else {
            return v;
        }
    }
    
    public @Override Map<String,String> getProperties() {
        return delegatingEvaluator(true).getProperties();
    }

    private boolean isModuleListDependentProperty(String p) {
        return p.equals("module.classpath") || // NOI18N
                p.equals(CP) || p.endsWith(".cp") || p.endsWith(".cp.extra"); // NOI18N
    }
    
    private static final Pattern ANT_PROP_REGEX = Pattern.compile("\\$\\{([a-zA-Z0-9._-]+)\\}"); // NOI18N
    private boolean isModuleListDependentValue(String v) {
        if (v == null) {
            return false;
        }
        Matcher m = ANT_PROP_REGEX.matcher(v);
        while (m.find()) {
            if (isModuleListDependentProperty(m.group(1))) {
                return true;
            }
        }
        return false;
    }
    
    public @Override void addPropertyChangeListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }
    
    public @Override void removePropertyChangeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }
    
    private PropertyEvaluator delegatingEvaluator(final boolean reset) {
        return ProjectManager.mutex().readAccess(new Mutex.Action<PropertyEvaluator>() {
            public @Override PropertyEvaluator run() {
                if (reset && !loadedModuleList) {
                    reset();
                    if (Util.err.isLoggable(ErrorManager.INFORMATIONAL)) {
                        Util.err.log("Needed to reset evaluator in " + project + "due to use of module-list-dependent property; now cp=" + delegate.getProperty(CP));
                    }
                }
                synchronized (Evaluator.this) {
                    return delegate;
                }
            }
        });
    }
    
    private void reset() {
        if (!project.getProjectDirectoryFile().exists()) {
            return; // recently deleted?
        }
        ProjectManager.mutex().readAccess(new Mutex.Action<Void>() {
            public @Override Void run() {
                final ModuleList moduleList;
                try {
                    moduleList = project.getModuleList();
                } catch (IOException e) {
                    Util.err.notify(ErrorManager.INFORMATIONAL, e);
                    // but leave old evaluator in place for now
                    return null;
                }
                synchronized (Evaluator.this) {
                    loadedModuleList = true;
                    delegate.removePropertyChangeListener(Evaluator.this);
                    delegate = createEvaluator(moduleList);
                    delegate.addPropertyChangeListener(Evaluator.this);
                }
                // XXX better to compute diff between previous and new values and fire just those
                pcs.firePropertyChange(null, null, null);
                return null;
            }
        });
    }
    
    public @Override void propertyChange(PropertyChangeEvent evt) {
        if (ModuleList.NETBEANS_DEST_DIR.equals(evt.getPropertyName()) || evt.getPropertyName() == null) {
            // Module list may have changed.
            reset();
        } else {
            Util.err.log("Refiring property change from delegate in " + evt.getPropertyName() + " for " + project);
            pcs.firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
        }
    }
    
    public @Override void configurationXmlChanged(AntProjectEvent ev) {
        if (ev.getPath().equals(AntProjectHelper.PROJECT_XML_PATH)) {
            if (runInAtomicAction) {
                pendingReset = true;
            } else {
                reset();
            }
        }
    }
    
    public @Override void propertiesChanged(AntProjectEvent ev) {
        /* TODO: Not needed now? Put here at least some comment. */
    }
    
    /** See issue #69440 for more details. */
    public void setRunInAtomicAction(boolean runInAtomicAction) {
        assert runInAtomicAction != this.runInAtomicAction : "Nested calls not supported";
        this.runInAtomicAction = runInAtomicAction;
        if (! runInAtomicAction && pendingReset) {
            reset();
        }
        pendingReset = false;
    }
    
    /** See issue #69440 for more details. */
    boolean isRunInAtomicAction() {
        return runInAtomicAction;
    }
    
    public void removeListeners() {
        project.getHelper().removeAntProjectListener(this);
        delegate.removePropertyChangeListener(this);
    }
    
    /**
     * Create a property evaluator: private project props, shared project props, various defaults.
     * Synch with nbbuild/templates/projectized.xml.
     * @param ml this module list, or may be left null to skip all properties which require knowledge of other modules
     */
    private PropertyEvaluator createEvaluator(ModuleList ml) {
        // XXX a lot of this duplicates ModuleList.parseProperties... can they be shared?
        PropertyProvider predefs = project.getHelper().getStockPropertyPreprovider();
        Map<String,String> stock = new HashMap<String,String>();
        File dir = project.getProjectDirectoryFile();
        NbModuleType type = project.getModuleType();
        PropertyProvider privateProperties = project.getHelper().getPropertyProvider(AntProjectHelper.PRIVATE_PROPERTIES_PATH);
        PropertyProvider projectProperties = project.getHelper().getPropertyProvider(AntProjectHelper.PROJECT_PROPERTIES_PATH);
        File nbroot;
        if (type == NbModuleType.NETBEANS_ORG) {
            nbroot = ModuleList.findNetBeansOrg(dir);
            assert nbroot != null : "netbeans.org-type module not in a complete netbeans.org source root " + dir;
            stock.put("nb_all", nbroot.getAbsolutePath()); // NOI18N
            // Only needed for netbeans.org modules, since for external modules suite.properties suffices.
            File destDir = ModuleList.findNetBeansOrgDestDir(nbroot);
            stock.put(ModuleList.NETBEANS_DEST_DIR, destDir.getAbsolutePath());
            stock.put("nb.build.dir", "${nb_all}/nbbuild/build"); // #199286
            stock.put("nbantext.jar", "${nb.build.dir}/nbantext.jar");
            // Register *.dir for nb.org modules. There is no equivalent for external modules.
            try {
                Map<String,String> clusterProperties = ModuleList.getClusterProperties(nbroot);
                StringBuilder allValsB = new StringBuilder();
                for (PropertyProvider pp : new PropertyProvider[] {privateProperties, projectProperties}) {
                    for (String val : pp.getProperties().values()) {
                        allValsB.append(val);
                    }
                }
                String allVals = allValsB.toString();
                for (Map.Entry<String,String> dirEntry : clusterProperties.entrySet()) {
                    String key = dirEntry.getKey();
                    if (!key.endsWith(".dir")) { // NOI18N
                        continue;
                    }
                    String clusterDir = new File(destDir, dirEntry.getValue()).getAbsolutePath()./* #48449 */intern();
                    String modules = clusterProperties.get(key.substring(0, key.length() - 4));
                    if (modules == null) {
                        continue;
                    }
                    for (String module : modules.split(",")) { // NOI18N
                        String dirProp = module + ".dir";
                        if (allVals.contains(dirProp)) {
                            stock.put(dirProp.intern(), clusterDir); // NOI18N
                        } // #172203: otherwise don't waste space on it
                    }
                }
            } catch (IOException x) {
                Util.err.notify(ErrorManager.INFORMATIONAL, x);
            }
        } else {
            nbroot = null;
        }

        List<PropertyProvider> providers = new ArrayList<PropertyProvider>();
        providers.add(PropertyUtils.fixedPropertyProvider(stock));
        // XXX should listen to changes in values of properties which refer to property files:
        if (type == NbModuleType.SUITE_COMPONENT) {
            providers.add(project.getHelper().getPropertyProvider("nbproject/private/suite-private.properties")); // NOI18N
            providers.add(project.getHelper().getPropertyProvider("nbproject/suite.properties")); // NOI18N
        }

        // 'cluster' prop. evaluation without scanned ModuleList
        String codeNameBase = project.getCodeNameBase();
        PropertyEvaluator suiteEval = null;
        if (type == NbModuleType.SUITE_COMPONENT) {
            suiteEval = PropertyUtils.sequentialPropertyEvaluator(predefs, providers.toArray(new PropertyProvider[providers.size()]));
        }

        if (type == NbModuleType.SUITE_COMPONENT) {
            String suiteDirS = suiteEval.getProperty("suite.dir"); // NOI18N
            if (suiteDirS != null) {
                File suiteDir = PropertyUtils.resolveFile(dir, suiteDirS);
                providers.add(PropertyUtils.propertiesFilePropertyProvider(new File(suiteDir, "nbproject" + File.separatorChar + "private" + File.separatorChar + "platform-private.properties"))); // NOI18N
                providers.add(PropertyUtils.propertiesFilePropertyProvider(new File(suiteDir, "nbproject" + File.separatorChar + "platform.properties"))); // NOI18N
            }
        } else if (type == NbModuleType.STANDALONE) {
            providers.add(project.getHelper().getPropertyProvider("nbproject/private/platform-private.properties")); // NOI18N
            providers.add(project.getHelper().getPropertyProvider("nbproject/platform.properties")); // NOI18N
        }
        if (type == NbModuleType.SUITE_COMPONENT || type == NbModuleType.STANDALONE) {
            PropertyEvaluator baseEval = PropertyUtils.sequentialPropertyEvaluator(predefs, providers.toArray(new PropertyProvider[providers.size()]));
            providers.add(new ApisupportAntUtils.UserPropertiesFileProvider(baseEval, dir));
            baseEval = PropertyUtils.sequentialPropertyEvaluator(predefs, providers.toArray(new PropertyProvider[providers.size()]));
            providers.add(new DestDirProvider(baseEval));
        }
        if (type == NbModuleType.NETBEANS_ORG) {
            // For local definitions of nbjdk.* properties:
            File nbbuild = new File(nbroot, "nbbuild"); // NOI18N
            providers.add(PropertyUtils.propertiesFilePropertyProvider(new File(nbbuild, "user.build.properties"))); // NOI18N
            providers.add(PropertyUtils.propertiesFilePropertyProvider(new File(nbbuild, "site.build.properties"))); // NOI18N
            providers.add(PropertyUtils.propertiesFilePropertyProvider(new File(System.getProperty("user.home"), ".nbbuild.properties"))); // NOI18N
        }
        PropertyEvaluator baseEval = PropertyUtils.sequentialPropertyEvaluator(predefs, providers.toArray(new PropertyProvider[providers.size()]));
        providers.add(new NbJdkProvider(baseEval));
        providers.add(privateProperties);
        providers.add(projectProperties);
        Map<String,String> defaults = new HashMap<String,String>();
        if (codeNameBase != null) { // #121856
            defaults.put("code.name.base.dashes", codeNameBase.replace('.', '-')); // NOI18N
        }
        defaults.put("module.jar.dir", "modules"); // NOI18N
        defaults.put("module.jar.basename", "${code.name.base.dashes}.jar"); // NOI18N
        defaults.put("module.jar", "${module.jar.dir}/${module.jar.basename}"); // NOI18N
        defaults.put("manifest.mf", "manifest.mf"); // NOI18N
        defaults.put("src.dir", "src"); // NOI18N
        defaults.put("build.dir", "build"); // NOI18N
        defaults.put("build.classes.dir", "${build.dir}/classes"); // NOI18N
        defaults.put("release.dir", "release"); // NOI18N
        defaults.put(SingleModuleProperties.JAVAC_SOURCE, "1.4"); // NOI18N
        if (type == NbModuleType.NETBEANS_ORG) {
            defaults.put("test.user.dir", "${nb_all}/nbbuild/testuserdir"); // NOI18N
        } else if (type == NbModuleType.STANDALONE) {
            defaults.put("test.user.dir", "${build.dir}/testuserdir"); // NOI18N
        } else {
            defaults.put("suite.build.dir", "${suite.dir}/build"); // NOI18N
            defaults.put("test.user.dir", "${suite.build.dir}/testuserdir"); // NOI18N
        }
        Set<String> testTypes = new HashSet<String>(Arrays.asList(NbModuleProject.COMMON_TEST_TYPES));
        // XXX would be good to add in any other types defined in project.xml
        for (String testType : testTypes) {
            defaults.put("test." + testType + ".src.dir", "test/" + testType + "/src"); // NOI18N
            defaults.put("test." + testType + ".data.dir", "test/" + testType + "/data"); // NOI18N
            defaults.put("build.test." + testType + ".classes.dir", "${build.dir}/test/" + testType + "/classes"); // NOI18N
        }
        providers.add(PropertyUtils.fixedPropertyProvider(defaults));
        try {
            providers.add(PropertyUtils.fixedPropertyProvider(Collections.singletonMap("cluster", ModuleList.findClusterLocation(dir, nbroot, type)))); // NOI18N
        } catch (IOException ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.INFO, null, ex);
        }
        if (ml != null) {
            providers.add(PropertyUtils.fixedPropertyProvider(Collections.singletonMap("module.classpath", computeModuleClasspath(ml)))); // NOI18N
            providers.add(PropertyUtils.fixedPropertyProvider(Collections.singletonMap("module.run.classpath", computeRuntimeModuleClasspath(ml)))); // NOI18N
            Map<String,String> buildDefaults = new HashMap<String,String>();
            buildDefaults.put("cp.extra", ""); // NOI18N
            buildDefaults.put(CP, "${module.classpath}:${cp.extra}"); // NOI18N
            buildDefaults.put(RUN_CP, "${module.run.classpath}:${cp.extra}:${build.classes.dir}"); // NOI18N
            if (type == NbModuleType.NETBEANS_ORG && "true".equals(projectProperties.getProperties().get("requires.nb.javac"))) {
                ModuleEntry javacapi = ml.getEntry("org.netbeans.libs.javacapi");
                if (javacapi != null) {
                    buildDefaults.put(ClassPathProviderImpl.BOOTCLASSPATH_PREPEND, javacapi.getClassPathExtensions());
                }
            }
            
            baseEval = PropertyUtils.sequentialPropertyEvaluator(predefs, providers.toArray(new PropertyProvider[providers.size()]));

            Map<String,TestClasspath> testsCPs = computeTestingClassPaths(ml, baseEval, testTypes);
            testTypes.addAll(testsCPs.keySet());
            for (String testType : testTypes) {
                buildDefaults.put("test." + testType + ".cp.extra", ""); // NOI18N
                TestClasspath tcp = TestClasspath.getOrEmpty(testsCPs, testType);
                // #165446: module.run.classpath on both compile and run test CPs
                String commonCPEntries = "${module.run.classpath}:${cp.extra}:${cluster}/${module.jar}:${test." + testType + ".cp.extra}:";    // NOI18N
                buildDefaults.put("test." + testType + ".cp", commonCPEntries + tcp.getCompileClasspath()); // NOI18N
                buildDefaults.put("test." + testType + ".run.cp.extra", ""); // NOI18N
                buildDefaults.put("test." + testType + ".run.cp", "${build.test." + testType + ".classes.dir}:" +
                        commonCPEntries + "${test." + testType + ".run.cp.extra}:" + tcp.getRuntimeClasspath()); // NOI18N
            }

            providers.add(PropertyUtils.fixedPropertyProvider(buildDefaults));
        }
        // skip a bunch of properties irrelevant here - NBM stuff, etc.
        return PropertyUtils.sequentialPropertyEvaluator(predefs, providers.toArray(new PropertyProvider[providers.size()]));
    }
    
    private static final RequestProcessor RP = new RequestProcessor(Evaluator.class.getName());
    private final class NbJdkProvider implements PropertyProvider, PropertyChangeListener { // #63541: JDK selection

        private final PropertyEvaluator eval;
        private final ChangeSupport changeSupport = new ChangeSupport(this);
        private final PropertyChangeListener weakListener = WeakListeners.propertyChange(this, null);
        
        public NbJdkProvider(PropertyEvaluator eval) {
            this.eval = eval;
            eval.addPropertyChangeListener(weakListener);
            JavaPlatformManager.getDefault().addPropertyChangeListener(weakListener);
        }
        
        public @Override final Map<String,String> getProperties() {
            Map<String,String> props = new HashMap<String,String>();
            String home = eval.getProperty(NBJDK_HOME);
            if (home == null) {
                String active = eval.getProperty(ModuleProperties.JAVA_PLATFORM_PROPERTY);
                if (active != null && !active.equals("default")) { // NOI18N
                    home = eval.getProperty("platforms." + active + ".home"); // NOI18N
                    if (home != null) {
                        props.put(NBJDK_HOME, home);
                    }
                }
            }
            if (home == null) {
                JavaPlatform platform = JavaPlatformManager.getDefault().getDefaultPlatform();
                if (platform != null) {
                    Collection<FileObject> installs = platform.getInstallFolders();
                    if (installs.size() == 1) {
                        home = FileUtil.toFile(installs.iterator().next()).getAbsolutePath();
                    }
                }
            }
            Object bootcp = null;
            if (home != null) {
                FileObject homeFO = FileUtil.toFileObject(FileUtil.normalizeFile(new File(home)));
                if (homeFO != null) {
                    for (JavaPlatform platform : JavaPlatformManager.getDefault().getInstalledPlatforms()) {
                        if (new HashSet<FileObject>(platform.getInstallFolders()).equals(Collections.singleton(homeFO))) {
                            // Matching JDK is registered, so look up its real bootcp.
                            ClassPath boot = platform.getBootstrapLibraries();
                            boot.removePropertyChangeListener(weakListener);
                            boot.addPropertyChangeListener(weakListener);
                            if (JDK9.compareTo(platform.getSpecification().getVersion()) <= 0) {
                                final Collection<? extends FileObject> loc = platform.getInstallFolders();
                                if (!loc.isEmpty()) {
                                    final File locf = FileUtil.toFile(loc.iterator().next());
                                    if (locf != null) {
                                        bootcp = locf;
                                        break;
                                    }
                                }
                            } else {
                                bootcp = boot.toString(ClassPath.PathConversionMode.WARN);
                                break;
                            }
                        }
                    }
                }
                if (bootcp == null) {
                    //TODO: Fixme for JDK9 - ProjectPlatform should be the solution
                    File jHome;
                    if (home != null && (jHome = new File(home, "jre/lib")).isDirectory()) {
                        String[] jars = jHome.list(new FilenameFilter() {
                            public @Override boolean accept(File dir, String name) {
                                String n = name.toLowerCase(Locale.US);
                                return n.endsWith(".jar"); // NOI18N
                            }
                        });
                        StringBuilder sb = new StringBuilder();
                        for (String jar : jars) {
                            if (sb.length() > 0) {
                                sb.append(File.pathSeparator);
                            }
                            sb.append("${" + NBJDK_HOME + "}/jre/lib/").append(jar);
                        }
                        bootcp = sb.toString().replace('/', File.separatorChar); // NOI18N
                    } else {
                        bootcp = "${" + NBJDK_HOME + "}/jre/lib/rt.jar".replace('/', File.separatorChar); // NOI18N
                    }
                }
            }
            if (bootcp == null) {
                // Real fallback...
                bootcp = "${sun.boot.class.path}"; // NOI18N
            }
            if (bootcp instanceof File) {
                props.remove(NBJDK_BOOTCLASSPATH);
                props.put(NBJDK_BOOTCLASSPATH_MODULAR, ((File) bootcp).getAbsolutePath());    //NOI18N
            } else if (bootcp instanceof String) {
                props.remove(NBJDK_BOOTCLASSPATH_MODULAR);
                props.put(NBJDK_BOOTCLASSPATH, (String)bootcp); // NOI18N
            }
            if (home != null) {
                String toolsJar = home + "/lib/tools.jar".replace('/', File.separatorChar);
                if (new File(toolsJar).exists()) { //On Mac OS X with Apple JDK, everything is in classes.jar, there is no tools.jar
                    props.put("tools.jar", toolsJar); // NOI18N
                }
            }
            if (Util.err.isLoggable(ErrorManager.INFORMATIONAL)) {
                Map<String,String> _props = new TreeMap<String,String>(eval.getProperties());
                Iterator<String> it = _props.keySet().iterator();
                while (it.hasNext()) {
                    String k = it.next();
                    if (!k.startsWith("nbjdk.") && !k.startsWith("platforms.")) { // NOI18N
                        it.remove();
                    }
                }
                _props.putAll(props);
                Util.err.log("JDK-related properties of " + project + ": " + _props);
            }
            return props;
        }
        
        public @Override final void addChangeListener(ChangeListener l) {
            changeSupport.addChangeListener(l);
        }
        
        public @Override final void removeChangeListener(ChangeListener l) {
            changeSupport.removeChangeListener(l);
        }
        
        public @Override final void propertyChange(PropertyChangeEvent evt) {
            String p = evt.getPropertyName();
            if (p != null && !p.startsWith("nbjdk.") && !p.startsWith("platforms.") && // NOI18N
                    !p.equals(ClassPath.PROP_ENTRIES) && !p.equals(JavaPlatformManager.PROP_INSTALLED_PLATFORMS)) {
                return;
            }
            if (!changeSupport.hasListeners()) {
                return;
            }
            final Mutex.Action<Void> action = new Mutex.Action<Void>() {
                public @Override Void run() {
                    changeSupport.fireChange();
                    return null;
                }
            };
            // See ProjectProperties.PP.fireChange for explanation of this threading stuff:
            if (ProjectManager.mutex().isWriteAccess()) {
                ProjectManager.mutex().readAccess(action);
            } else if (ProjectManager.mutex().isReadAccess()) {
                action.run();
            } else {
                RP.post(new Runnable() {
                    public @Override void run() {
                        ProjectManager.mutex().readAccess(action);
                    }
                });
            }
        }
        
    }
    
    /**
     * Should be similar to impl in ParseProjectXml.
     */
    private String computeModuleClasspath(ModuleList ml) {
        Element data = project.getPrimaryConfigurationData();
        Element moduleDependencies = XMLUtil.findElement(data,
            "module-dependencies", NbModuleProject.NAMESPACE_SHARED); // NOI18N
        assert moduleDependencies != null : "Malformed metadata in " + project;
        StringBuilder cp = new StringBuilder();
        for (Element dep : XMLUtil.findSubElements(moduleDependencies)) {
            if (XMLUtil.findElement(dep, "compile-dependency", // NOI18N
                    NbModuleProject.NAMESPACE_SHARED) == null) {
                continue;
            }
            Element cnbEl = XMLUtil.findElement(dep, "code-name-base", // NOI18N
                NbModuleProject.NAMESPACE_SHARED);
            String cnb = XMLUtil.findText(cnbEl);
            ModuleEntry module = ml.getEntry(cnb);
            if (module == null) {
                Util.err.log(ErrorManager.WARNING, "Warning - could not find dependent module " + cnb + " for " + FileUtil.getFileDisplayName(project.getProjectDirectory()));
                continue;
            }
            File moduleJar = module.getJarLocation();
            if (cp.length() > 0) {
                cp.append(File.pathSeparatorChar);
            }
            cp.append(moduleJar.getAbsolutePath());
            cp.append(module.getClassPathExtensions());
        }
        appendMyOwnClassPathExtensions(cp);
        return cp.toString();
    }
    
    /**
     * Follows transitive runtime dependencies.
     * @see "issue #70206"
     */
    private String computeRuntimeModuleClasspath(ModuleList ml) {
        Set<String> unprocessed = new HashSet<String>();
        unprocessed.add(project.getCodeNameBase());
        Set<String> processed = new HashSet<String>();
        StringBuilder cp = new StringBuilder();
        if(cachedProjectModuleEntries.get(project) == null) {
            cachedProjectModuleEntries.put(project, new HashMap<String, SoftReference<ModuleEntry>>());
        }
        Map<String, SoftReference<ModuleEntry>> cachedModuleEntries = cachedProjectModuleEntries.get(project);
        while (!unprocessed.isEmpty()) { // crude breadth-first search
            Iterator<String> it = unprocessed.iterator();
            String cnb = it.next();
            it.remove();
            if (processed.add(cnb)) {
                ModuleEntry module = null;
                if(cachedModuleEntries.get(cnb) != null && cachedModuleEntries.get(cnb).get() != null) {
                    module = cachedModuleEntries.get(cnb).get();
                }
                else {
                    module = ml.getEntry(cnb);
                    cachedProjectModuleEntries.get(project).put(cnb, new SoftReference<ModuleEntry>(module));
                }
                if (module == null) {
                    Util.err.log(ErrorManager.WARNING, "Warning - could not find dependent module " + cnb + " for " + FileUtil.getFileDisplayName(project.getProjectDirectory()));
                    continue;
                }
                if (!cnb.equals(project.getCodeNameBase())) { // build/classes for this is special
                    if (cp.length() > 0) {
                        cp.append(File.pathSeparatorChar);
                    }
                    cp.append(module.getJarLocation().getAbsolutePath());
                    cp.append(module.getClassPathExtensions());
                }
                String[] newDeps = module.getRunDependencies();
                unprocessed.addAll(Arrays.asList(newDeps));
            }
        }
        appendMyOwnClassPathExtensions(cp); // #76341: must include <class-path-extension>s in ${run.cp} too.
        return cp.toString();
    }

    private void appendMyOwnClassPathExtensions(StringBuilder cp) {
        // XXX #179578: using ModuleEntry.getClassPathExtensions would be more convenient, but the data is stale;
        // should ModuleList recreate ModuleEntry's when project.xml (or project.properties, ...) changes?
        Map<String,String> cpext = new ProjectXMLManager(project).getClassPathExtensions();
        for (Map.Entry<String,String> entry : cpext.entrySet()) {
            if (cp.length() > 0) {
                cp.append(File.pathSeparatorChar);
            }
            String binaryOrigin = entry.getValue();
            if (binaryOrigin != null) {
                cp.append(project.getHelper().resolveFile(binaryOrigin));
            } else {
                cp.append(PropertyUtils.resolveFile(project.getModuleJarLocation().getParentFile(), entry.getKey()));
            }
        }
    }

    /**
     * Gives a map from test type (e.g. <em>unit</em> or <em>qa-functional</em>)
     * to the {@link TestClasspath test classpath} according to the content in
     * the project's metadata (<em>project.xml<em>).
     */
    private Map<String,TestClasspath> computeTestingClassPaths(ModuleList ml, PropertyEvaluator evaluator, Set<String> extraTestTypes) {
        Map<String, TestClasspath> classpaths = new HashMap<String,TestClasspath>();
        ProjectXMLManager pxm = new ProjectXMLManager(project);
        Map<String, Set<TestModuleDependency>> testDependencies = pxm.getTestDependencies(ml);
        
        String testDistDir =  evaluator.getProperty("test.dist.dir"); // NOI18N
        if (testDistDir == null) {
            NbModuleType type = project.getModuleType();
            if (type == NbModuleType.NETBEANS_ORG) {
                // test.dist.dir = ${nb_all}/nbbuild/build/testdist
                String nball = evaluator.getProperty("nb_all"); // NOI18N
                testDistDir = nball + File.separatorChar + "nbbuild" + File.separatorChar + "build" + File.separatorChar + "testdist"; // NOI18N
            } else if ( type == NbModuleType.SUITE_COMPONENT) {
                // test.dist.dir = ${suite.build.dir}/testdist
                String suiteDir = evaluator.getProperty("suite.build.dir"); // NOI18N
                testDistDir = suiteDir + File.separatorChar + "testdist"; // NOI18N
            } else {
                // standalone module
                // test.dist.dir = ${build.dir}/testdist
                String moduleDir = evaluator.getProperty("build.dir"); // NOI18N
                testDistDir = moduleDir + File.separatorChar + "testdist"; // NOI18N
            }
        }
        for (Map.Entry<String,Set<TestModuleDependency>> entry : testDependencies.entrySet()) {
            computeTestType(entry.getKey(), new File(testDistDir), entry.getValue(), classpaths, ml);
        }
        for (String testType : extraTestTypes) {
            if (!testDependencies.containsKey(testType)) {
                // No declared dependencies of this type, so will definitely need to add in compatibility libraries.
                computeTestType(testType, new File(testDistDir), Collections.<TestModuleDependency>emptySet(), classpaths, ml);
            }
        }
        return classpaths;
    }
    
    private void computeTestType(String ttName, File testDistDir, Set<TestModuleDependency> ttModules, Map<String,TestClasspath> classpaths, ModuleList ml) {
        final Set<String> compileCnbs = new HashSet<String>();
        final Set<String> runtimeCnbs = new HashSet<String>();
        final Set<String> testCompileCnbs = new HashSet<String>();
        final Set<String> testRuntimeCnbs = new HashSet<String>();
        Logger logger = Logger.getLogger(Evaluator.class.getName());
        // maps CNB->status: FALSE - added only to runtime CP, TRUE - added to compile CP too,
        // null - not processed (yet)
        Map<String, Boolean> processedRecursive = new HashMap<String, Boolean>();

        // #139339: optimization using processedRecursive set was too bold, removed
        for (TestModuleDependency td : ttModules) {
            String cnb = td.getModule().getCodeNameBase();
            logger.log(Level.FINE, "computeTestType: processing ''{0}''", cnb);
            if (td.isRecursive()) {
                // scan cp recursively
                Set<String> unprocessed = new HashSet<String>();

                final String codeNameBase = project.getCodeNameBase();
                unprocessed.add(td.getModule().getCodeNameBase());
                while (!unprocessed.isEmpty()) { // crude breadth-first search
                    Iterator<String> it = unprocessed.iterator();
                    String recursiveCNB = it.next();
                    it.remove();

                    // if we've put recursiveCNB module only to runtime CP and now should be also added to compile CP, process once more
                    Boolean alreadyInCompileCP = processedRecursive.get(recursiveCNB);
                    if (Boolean.TRUE.equals(alreadyInCompileCP)
                            || (Boolean.FALSE.equals(alreadyInCompileCP) && ! td.isCompile())) {
                        continue;
                    }

                    logger.log(Level.FINE, "computeTestType: processing ''{0}''", recursiveCNB);
                    ModuleEntry module = ml.getEntry(recursiveCNB);
                    if (module == null) {
                        Util.err.log(ErrorManager.WARNING, "Warning - could not find dependent module " + recursiveCNB + " for " + FileUtil.getFileDisplayName(project.getProjectDirectory()));
                        continue;
                    }
                    if (!recursiveCNB.equals(codeNameBase)) { // build/classes for this is special
//                       XXX clb.callback(td, cnb);
                        runtimeCnbs.add(recursiveCNB);
                        if (td.isCompile()) {
                            compileCnbs.add(recursiveCNB);
                        }
                        processedRecursive.put(recursiveCNB, td.isCompile());
                    }
                    String[] newDeps = module.getRunDependencies();
                    unprocessed.addAll(Arrays.asList(newDeps));
                }
//             XXX   processTestEntryRecursive(td,
//                        new Callback() {
//                            public void callback(TestModuleDependency td, String cnb) {
//                            }
//                        }, ml);
            } else {
                runtimeCnbs.add(cnb);
                if (td.isCompile()) {
                    compileCnbs.add(cnb);
                }
            }
            if (td.isTest()) {
                if (td.isCompile()) {
                    testCompileCnbs.add(cnb);
                }
                testRuntimeCnbs.add(cnb);
            }
        }

        StringBuilder extra = new StringBuilder();
        TestClasspath testClasspath = new TestClasspath(
                mergePaths(compileCnbs,false,ttName,testDistDir, ml) + extra,
                mergePaths(runtimeCnbs,false,ttName,testDistDir,ml) + extra,
                mergePaths(testCompileCnbs,true,ttName,testDistDir,ml),
                mergePaths(testRuntimeCnbs,true,ttName,testDistDir,ml));

        classpaths.put(ttName,testClasspath);
    }

   private static final Set<String> warnedModules = Collections.synchronizedSet(new HashSet<String>());
    private String mergePaths(Set<String> cnbs, boolean test,String testtype,File testDistDir,ModuleList ml) {
        StringBuilder cps = new StringBuilder();
        for (String cnb : cnbs) {
                ModuleEntry module = ml.getEntry(cnb);
                if (module == null) {
                    if (warnedModules.add(cnb)) {
                        Logger.getLogger(Evaluator.class.getName()).log(Level.WARNING, "Cannot find test module dependency: {0}", cnb);
                    }
                    continue;
                }
                if (cps.length() > 0) {
                    cps.append(':');
                }
                if (test) {
                    // we need to get cluster name
                    File clusterDir = module.getClusterDirectory();
                    if (clusterDir != null) {
                        String clusterName = clusterDir.getName();
                        char s = File.separatorChar;
                        File jarFile = new File(
                                          testDistDir, testtype + s + clusterName + s + cnb.replace('.','-') + s + "tests.jar"); // NOI18N
                        cps.append(jarFile.getPath());
                        // See ParseProjectXml:
                        if (!testtype.equals("unit")) {
                            cps.append(':');
                            jarFile = new File(testDistDir, "unit" + s + clusterName + s + cnb.replace('.', '-') + s + "tests.jar"); // NOI18N
                            cps.append(jarFile.getPath());
                        }
                    }
                     
                } else {
                    cps.append(module.getJarLocation().getPath());
                    cps.append(module.getClassPathExtensions()); // #105621
                }
        }
        return cps.toString();
    }
   
}
