All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.efaps.update.version.Application Maven / Gradle / Ivy

/*
 * Copyright 2003 - 2012 The eFaps Team
 *
 * 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.
 *
 * Revision:        $Rev: 7908 $
 * Last Changed:    $Date: 2012-08-13 16:39:28 -0500 (Mon, 13 Aug 2012) $
 * Last Changed By: $Author: [email protected] $
 */

package org.efaps.update.version;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.annotations.FromAnnotationsRuleModule;
import org.apache.commons.digester3.annotations.rules.BeanPropertySetter;
import org.apache.commons.digester3.annotations.rules.CallMethod;
import org.apache.commons.digester3.annotations.rules.CallParam;
import org.apache.commons.digester3.annotations.rules.ObjectCreate;
import org.apache.commons.digester3.annotations.rules.SetNext;
import org.apache.commons.digester3.annotations.rules.SetProperty;
import org.apache.commons.digester3.binder.DigesterLoader;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.tools.ant.DirectoryScanner;
import org.efaps.admin.datamodel.Type;
import org.efaps.admin.runlevel.RunLevel;
import org.efaps.ci.CIAdminCommon;
import org.efaps.db.Context;
import org.efaps.db.Insert;
import org.efaps.db.InstanceQuery;
import org.efaps.db.QueryBuilder;
import org.efaps.update.FileType;
import org.efaps.update.Install;
import org.efaps.update.Profile;
import org.efaps.update.schema.program.esjp.ESJPCompiler;
import org.efaps.update.schema.program.staticsource.AbstractStaticSourceCompiler;
import org.efaps.update.util.InstallationException;
import org.efaps.util.EFapsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author The eFaps Team
 * @version $Id: Application.java 7908 2012-08-13 21:39:28Z [email protected] $
 */
@ObjectCreate(pattern = "install")
public final class Application
{

    /**
     * Logging instance used to give logging information of this class.
     */
    private static final Logger LOG = LoggerFactory.getLogger(Application.class);

    /**
     * Default Mapping of a a file extension to a Type for import and update.
     */
    private static final Map DEFAULT_TYPE_MAPPING = new HashMap();
    static {
        Application.DEFAULT_TYPE_MAPPING.put("css", FileType.CSS.getType());
        Application.DEFAULT_TYPE_MAPPING.put("java", FileType.JAVA.getType());
        Application.DEFAULT_TYPE_MAPPING.put("js", FileType.JS.getType());
        Application.DEFAULT_TYPE_MAPPING.put("jrxml", FileType.JRXML.getType());
        Application.DEFAULT_TYPE_MAPPING.put("wiki", FileType.WIKI.getType());
        Application.DEFAULT_TYPE_MAPPING.put("xml", FileType.XML.getType());
        Application.DEFAULT_TYPE_MAPPING.put("xsl", FileType.XSL.getType());
    }

    /**
     * Default list of includes used to evaluate the files to copy.
     *
     * @see #getFiles
     */
    private static final Set DEFAULT_INCLUDES = new HashSet();
    static {
        Application.DEFAULT_INCLUDES.add("**/*.css");
        Application.DEFAULT_INCLUDES.add("**/*.java");
        Application.DEFAULT_INCLUDES.add("**/*.js");
        Application.DEFAULT_INCLUDES.add("**/*.jrxml");
        Application.DEFAULT_INCLUDES.add("**/*.wiki");
        Application.DEFAULT_INCLUDES.add("**/*.xml");
        Application.DEFAULT_INCLUDES.add("**/*.xsl");
    }

    /**
     * Default list of excludes used to evaluate the files to copy.
     *
     * @see #getFiles
     */
    private static final Set DEFAULT_EXCLUDES = new HashSet();
    static {
        Application.DEFAULT_EXCLUDES.add("**/versions.xml");
        Application.DEFAULT_EXCLUDES.add("**/package-info.java");
    }

    /**
     * Stores the name of the application.
     *
     * @see #setApplication
     */
    @BeanPropertySetter(pattern = "install/application")
    private String application = null;


    /**
     * Stores all versions of this application which must be installed.
     *
     * @see #getVersions()
     */
    private final Set versions = new TreeSet();

    /**
     * Install instance holding all XML definition / update files.
     *
     * @see #addURL(URL, String)
     */
    private final Install install = new Install();

    /**
     * Caches not stores versions (because if the kernel install is made, the
     * version could not be updated till the SQL tables and the data model is
     * already installed and the cache is reloaded).
     */
    private final List notStoredVersions = new ArrayList();

    /**
     * Stores the highest or maximum number of the versions to be installed.
     */
    private Long maxVersion;

    /**
     * Project class path.
     *
     * @see #Application(URL, List)
     * @see #getClassPathElements()
     */
    private List classpathElements;

    /**
     * Dependencies to other applications for this application ordered.
     */
    private final List dependencies = new ArrayList();

    /**
     * Root URL where the source files are located. Could be a file directory (
     * for local installation) or a jar file.
     *
     * @see #Application(URL, List)
     * @see #getRootUrl()
     */
    private URL rootUrl;

    /**
     * Stores the name of the rootPackage.
     */
    @SetProperty(pattern = "install/rootPackage", attributeName = "name")
    private String rootPackageName;


    /**
     * USed in combination with the digester.
     */
    private Map tmpElements = new HashMap();

    /**
     * Initializes the {@link #rootUrl root URL} of this application.
     *
     * @param _rootUrl root URL of the source
     * @param _classpathElements elements of the class path
     * @see #rootUrl
     */
    private Application(final URL _rootUrl,
                        final List _classpathElements)
    {
        this.rootUrl = _rootUrl;
        this.classpathElements = _classpathElements;
    }

    /**
     * Constructor used by Digester.
     */
    public Application()
    {
    }

    /**
     * null is returned, of the version file could not be opened
     * and read.
     *
     * @param _versionUrl URL of the version file which defines the application
     * @param _rootUrl root URL where the source files are located (for local
     *            files); URL of the class file (if source is in a Jar file)
     * @param _classpathElements elements of the class path
     * @return application instance with all version information
     * @throws InstallationException if version XML file could not be parsed
     *             TODO: description TODO: better definition of include dir /
     *             file
     */
    public static Application getApplication(final URL _versionUrl,
                                             final URL _rootUrl,
                                             final List _classpathElements)
        throws InstallationException
    {
        Application appl = null;
        try {
            final DigesterLoader loader = DigesterLoader.newLoader(new FromAnnotationsRuleModule()
            {
                @Override
                protected void configureRules()
                {
                    bindRulesFrom(Application.class);
                }
            });
            final Digester digester = loader.newDigester();
            appl = (Application) digester.parse(_versionUrl);
            appl.rootUrl = _rootUrl;
            appl.classpathElements = _classpathElements;
            for (final Entry entry : appl.tmpElements.entrySet()) {
                appl.addURL(new URL(_rootUrl, entry.getKey()), entry.getValue());
            }
            appl.tmpElements = null;
            Collections.sort(appl.dependencies, new Comparator()
            {

                @Override
                public int compare(final Dependency _dependency0,
                                   final Dependency _dependency1)
                {
                    return _dependency0.getOrder().compareTo(_dependency1.getOrder());
                }
            });
            for (final ApplicationVersion applVers : appl.getVersions()) {
                applVers.setApplication(appl);
                appl.setMaxVersion(applVers.getNumber());
            }

        } catch (final IOException e) {
            if (e.getCause() instanceof InstallationException) {
                throw (InstallationException) e.getCause();
            } else {
                throw new InstallationException("Could not open / read version file '" + _versionUrl + "'");
            }
          //CHECKSTYLE:OFF
        } catch (final Exception e) {
          //CHECKSTYLE:ON
            throw new InstallationException("Error while parsing file '" + _versionUrl + "'", e);
        }
        return appl;
    }

    /**
     * Returns the application definition read from a source directory.
     *
     * @param _versionFile          version file which defines the application
     * @param _classpathElements    class path elements (required to compile)
     * @param _eFapsDir             root directory with the XML installation
     *                              files
     * @param _outputDir            directory used as target for generated code
     * @param _includes list of includes; if null
     *            {@link #DEFAULT_INCLUDES} are used
     * @param _excludes list of excludes; if null
     *            {@link #DEFAULT_EXCLUDES} are used
     * @param _file2typeMapping mapping of file extension to type; if
     *            null {@link #DEFAULT_TYPE_MAPPING} is used
     * @return application instance with all version information
     * @throws InstallationException if version file could not be read or opened
     */
    public static Application getApplicationFromSource(final File _versionFile,
                                                       final List _classpathElements,
                                                       final File _eFapsDir,
                                                       final File _outputDir,
                                                       final List _includes,
                                                       final List _excludes,
                                                       final Map _file2typeMapping)
        throws InstallationException
    {
        final Map file2typeMapping = (_file2typeMapping == null)
                        ? Application.DEFAULT_TYPE_MAPPING
                        : _file2typeMapping;
        final Application appl;
        try {
            appl = Application.getApplication(_versionFile.toURI().toURL(),
                                _eFapsDir.toURI().toURL(),
                                _classpathElements);

            for (final String fileName : Application.getFiles(_eFapsDir, _includes, _excludes)) {
                final String type = file2typeMapping.get(fileName.substring(fileName.lastIndexOf(".") + 1));
                appl.addURL(new File(_eFapsDir, fileName).toURI().toURL(), type);
            }
            if (_outputDir.exists()) {
                for (final String fileName : Application.getFiles(_outputDir, _includes, _excludes)) {
                    final String type = file2typeMapping.get(fileName.substring(fileName.lastIndexOf(".") + 1));
                    appl.addURL(new File(_outputDir, fileName).toURI().toURL(), type);
                }
            }
        } catch (final IOException e) {
            throw new InstallationException("Could not open / read version file " + "'" + _versionFile + "'", e);
          //CHECKSTYLE:OFF
        } catch (final Exception e) {
          //CHECKSTYLE:ON
            throw new InstallationException("Read version file '" + _versionFile + "' failed", e);
        }
        return appl;
    }

    /**
     * Uses the _includes and _excludes together with
     * the root directory _eFapsDir to get all related and matched
     * files.
     *
     * @param _eFapsDir root directory where the file are located
     * @param _includes defines includes; if not specified the default value is
     *            **/*.xml
     * @param _excludes defines excludes; if not specified the default value is
     *            **/version.xml
     * @return array of file names
     * @see #DEFAULT_INCLUDES
     * @see #DEFAULT_EXCLUDES
     */
    protected static String[] getFiles(final File _eFapsDir,
                                       final List _includes,
                                       final List _excludes)
    {
        final DirectoryScanner ds = new DirectoryScanner();
        final String[] included = (_includes == null)
                        ? Application.DEFAULT_INCLUDES.toArray(new String[Application.DEFAULT_INCLUDES.size()])
                        : _includes.toArray(new String[_includes.size()]);
        final String[] excluded = (_excludes == null)
                        ? Application.DEFAULT_EXCLUDES.toArray(new String[Application.DEFAULT_EXCLUDES.size()])
                        : _excludes.toArray(new String[_excludes.size()]);
        ds.setIncludes(included);
        ds.setExcludes(excluded);
        ds.setBasedir(_eFapsDir.toString());
        ds.setCaseSensitive(true);
        ds.scan();

        return ds.getIncludedFiles();
    }

    /**
     * Method to get the applications from the class path.
     *
     * @param _application searched application in the class path
     * @param _classpath class path (list of the complete class path)
     * @return List of applications
     * @throws InstallationException if the install.xml file in the class path
     *             could not be accessed
     */
    public static Application getApplicationFromClassPath(final String _application,
                                                          final List _classpath)
        throws InstallationException
    {
        final ClassLoader cl = Application.class.getClassLoader();

        // get install application (read from all install xml files)
        final Map appls = new HashMap();
        try {
            final Enumeration urlEnum = cl.getResources("META-INF/efaps/install.xml");
            while (urlEnum.hasMoreElements()) {
                // TODO: why class path?
                final URL url = urlEnum.nextElement();
                final Application appl = Application.getApplication(url, new URL(url, "../../../"), _classpath);
                appls.put(appl.getApplication(), appl);
            }
        } catch (final IOException e) {
            throw new InstallationException("Could not access the install.xml file "
                            + "(in path META-INF/efaps/ path of each eFaps install jar).", e);
        }

        return appls.get(_application);
    }

    /**
     * Returns the application read from given JAR file _jarFile.
     *
     * @param _jarFile JAR file with the application to install
     * @param _classpath class path (required to compile)
     * @return application instance
     * @throws InstallationException if application could not be fetched from
     *             the JAR file or the version XML file could not be parsed
     */
    public static Application getApplicationFromJarFile(final File _jarFile,
                                                        final List _classpath)
        throws InstallationException
    {
        try {
            final URL url = new URL("jar", null, 0, _jarFile.toURI().toURL().toString() + "!/");
            final URL url2 = new URL(url, "/META-INF/efaps/install.xml");
            return Application.getApplication(url2, new URL(url2, "../../../"), _classpath);
        } catch (final IOException e) {
            throw new InstallationException("URL could not be parsed", e);
        }
    }

    /**
     * Compiles the ESJP's and all Cascade Styles Sheets within eFaps.
     *
     * @param _userName name of logged in user for which the compile is done
     *            (could be also null)
     * @param _classpath class path elements
     * @param _addRuntimeClassPath must the classpath from the runtime be added
     *                  to the classpath also
     * @throws InstallationException if reload cache of compile failed
     * @see #compileAll(String)
     */
    public static void compileAll(final String _userName,
                                  final List _classpath,
                                  final boolean _addRuntimeClassPath)
        throws InstallationException
    {
        (new Application((URL) null, _classpath)).compileAll(_userName, _addRuntimeClassPath);
    }

    /**
     * Compiles the ESJP's and all Cascade Styles Sheets within eFaps.
     *
     * @param _userName name of logged in user for which the compile is done
     *            (could be also null)
     * @param _addRuntimeClassPath must the classpath from the runtime be added
     *                  to the classpath also
     * @throws InstallationException if reload cache of compile failed
     */
    public void compileAll(final String _userName,
                           final boolean _addRuntimeClassPath)
        throws InstallationException
    {
        if (Application.LOG.isInfoEnabled()) {
            Application.LOG.info("..Compiling");
        }

        reloadCache();

        try {
            Context.begin(_userName);
            try {
                new ESJPCompiler(this.classpathElements).compile(null, _addRuntimeClassPath);
            } catch (final InstallationException e) {
                Application.LOG.error(" error during compilation of ESJP.");
            }
            AbstractStaticSourceCompiler.compileAll(this.classpathElements);
            Context.commit();
        } catch (final EFapsException e) {
            throw new InstallationException("Compile failed", e);
        }
    }

    /**
     * Installs current application including existing {@link #dependencies}.
     *
     * @param _userName name of logged in user
     * @param _password password of logged in user
     * @throws InstallationException for all cases the installation failed
     * @see #install(String, String, boolean)
     */
    public void install(final String _userName,
                        final String _password,
                        final Set _profiles)
        throws InstallationException
    {
        this.install(_userName, _password, _profiles, true);
    }

    /**
     * For each version in {@link #versions} is tested, if it is already
     * installed. If not already installed, the version is installed. Only if
     * _withDependency is defined, also the {@link #dependencies}
     * are installed.
     *
     * @param _userName name of the installation user
     * @param _password password of the installation user
     * @param _withDependency must the dependency also installed?
     * @throws InstallationException if installation failed
     */
    protected void install(final String _userName,
                           final String _password,
                           final Set _profiles,
                           final boolean _withDependency
                           )
        throws InstallationException
    {
        // install dependency if required
        if (_withDependency) {
            for (final Dependency dependency : this.dependencies) {
                dependency.resolve();
                final Application appl = Application.getApplicationFromJarFile(
                                dependency.getJarFile(), this.classpathElements);
                appl.install(_userName, _password, dependency.getProfiles(), false);
            }
        }

        // reload cache (if possible)
        reloadCache();

        // load latest installed versions
        final Map latestVersions;
        try {
            Context.begin();
            latestVersions = this.install.getLatestVersions();
            Context.rollback();
        } catch (final EFapsException e) {
            throw new InstallationException("Could not get information about installed versions", e);
        }
        final Integer latestVersion = latestVersions.get(this.application);

        Application.LOG.info("Install application '" + this.application + "'");

        for (final ApplicationVersion version : this.versions) {
            if (Application.LOG.isInfoEnabled()) {
                Application.LOG.info("Check version " + version.getNumber());
            }
            if ((latestVersion != null) && (version.getNumber() < latestVersion)) {
                if (Application.LOG.isInfoEnabled()) {
                    Application.LOG.info("Version " + version.getNumber() + " already installed");
                }
            } else {
                if (Application.LOG.isInfoEnabled()) {
                    Application.LOG.info("Starting installation of version " + version.getNumber());
                    final String desc = version.getDescription();
                    if (!"".equals(desc)) {
                        Application.LOG.info(desc);
                    }
                }
                try {
                    // TODO: correct exception handling in the installation
                    version.install(this.install, getLastVersion().getNumber(), _profiles, _userName, _password);
                  //CHECKSTYLE:OFF
                } catch (final Exception e) {
                  //CHECKSTYLE:ON
                    throw new InstallationException("Installation failed", e);
                }
                storeVersion(_userName, version.getNumber());

                if (Application.LOG.isInfoEnabled()) {
                    Application.LOG.info("Finished installation of version " + version.getNumber());
                }
            }
        }

        // reload cache (if possible)
        reloadCache();
    }

    /**
     * Updates the last installed version.
     *
     * @param _userName name of logged in user
     * @param _password password of logged in user TODO: throw Exceptions
     *            instead of logging errors
     * @param _profiles Profiles to be applied
     * @throws Exception on error
     */
    public void updateLastVersion(final String _userName,
                                  final String _password,
                                  final Set _profiles)
        throws Exception
    {
        // reload cache (if possible)
        reloadCache();

        // load installed versions
        Context.begin();
        final Map latestVersions = this.install.getLatestVersions();
        Context.rollback();
        final long latestVersion = latestVersions.get(this.application);

        final ApplicationVersion version = getLastVersion();
        if (version.getNumber() == latestVersion) {
            if (Application.LOG.isInfoEnabled()) {
                Application.LOG.info("Update version "
                                + version.getNumber()
                                + " of application '"
                                + this.application
                                + "'");
            }
            version.install(this.install, version.getNumber(), _profiles, _userName, _password);
            if (Application.LOG.isInfoEnabled()) {
                Application.LOG.info("Finished update of version " + version.getNumber());
            }
        } else {
            Application.LOG.error("Version "
                            + version.getNumber()
                            + " of application '"
                            + this.application
                            + "' not installed and could not updated!");
        }
    }

    /**
     * Store for this application that the version is already installed. If data
     * model in the local type cache is not loaded (because, e.g., it is a new
     * kernel install), the version numbers are cached.
* The first time, the version type could be get from the type cache, all * cached versions are stored in eFaps. * * @param _userName logged in user name * @param _version version id to store * @throws InstallationException if version could not be stored */ protected void storeVersion(final String _userName, final Long _version) throws InstallationException { final Type versionType = CIAdminCommon.Version.getType(); if (versionType != null) { try { Context.begin(_userName); // store cached versions for (final Long version : this.notStoredVersions) { final Insert insert = new Insert(versionType); insert.add(CIAdminCommon.Version.Name, this.application); insert.add(CIAdminCommon.Version.Revision, version); insert.execute(); } this.notStoredVersions.clear(); final QueryBuilder queryBldr = new QueryBuilder(CIAdminCommon.Version); queryBldr.addWhereAttrEqValue(CIAdminCommon.Version.Name, this.application); queryBldr.addWhereAttrEqValue(CIAdminCommon.Version.Revision, _version); final InstanceQuery query = queryBldr.getQuery(); query.execute(); if (!query.next()) { // store current version final Insert insert = new Insert(CIAdminCommon.Version); insert.add(CIAdminCommon.Version.Name, this.application); insert.add(CIAdminCommon.Version.Revision, _version); insert.execute(); } Context.commit(); } catch (final EFapsException e) { throw new InstallationException("Update of the version information failed", e); } } else { // if version could not be stored, cache the version information this.notStoredVersions.add(_version); } } /** * * @param _dependency dependency */ @SetNext public void addDependency(final Dependency _dependency) { this.dependencies.add(_dependency); } /** * Reloads the eFaps cache. * * @throws InstallationException if reload of the cache failed */ protected void reloadCache() throws InstallationException { try { Context.begin(); if (RunLevel.isInitialisable()) { RunLevel.init("shell"); RunLevel.execute(); } Context.rollback(); } catch (final EFapsException e) { throw new InstallationException("Reload cache failed", e); } } /** * Adds a n ew application version to this application which should be * installed. * * @param _version new application version to add */ @SetNext public void addVersion(final ApplicationVersion _version) { this.versions.add(_version); } /** * Returns the last application version which must be installed. * * @return last application version to install */ public ApplicationVersion getLastVersion() { return (ApplicationVersion) this.versions.toArray()[this.versions.size() - 1]; } /** * Setter method for instance variable {@link #application}. * * @param _application value for instance variable {@link #application} */ public void setApplication(final String _application) { this.application = _application; } /** * Adds a new URL with the XML definition file. * * @param _url url of XML definition files used to install * @param _type type of the URL * @see #install(String, String) */ public void addURL(final URL _url, final String _type) { this.install.addFile(_url, _type); } /** * Searches for the given file name (parameter _classPathFile) in the class * path and adds them as URL to the list of XML installation / update / * definition files ({@link #install}). * * @param _classPathFile file name from the class path to add * @param _type type of the file to be added * @throws MalformedURLException on error with the URL * @see #addURL(URL, String) */ @CallMethod(pattern = "install/files/file") public void addClassPathFile( @CallParam(pattern = "install/files/file", attributeName = "name") final String _classPathFile, @CallParam(pattern = "install/files/file", attributeName = "type") final String _type) throws MalformedURLException { this.tmpElements.put(_classPathFile, _type); } /** * Getter method for the instance variable {@link #install}. * * @return value of instance variable {@link #install} */ public Install getInstall() { return this.install; } /** * Getter method for the instance variable {@link #dependencies}. * * @return value of instance variable {@link #dependencies} */ public List getDependencies() { return this.dependencies; } /** * This is the getter method for instance variable {@link #application}. * * @return value of instance variable {@link #application} * @see #application */ public String getApplication() { return this.application; } /** * Returns all class path elements required to compile. * * @return class path elements * @see #classpathElements */ public List getClassPathElements() { return this.classpathElements; } /** * Returns the root URL where the source is located. For local sources it is * an URL to a directory, for a Jar file it is the URL to the Jar file. * * @return value of instance variable {@link #rootUrl} */ public URL getRootUrl() { return this.rootUrl; } /** * This is the getter method for instance variable {@link #versions}. * * @return value of instance variable {@link #versions} * @see #versions */ public Set getVersions() { return this.versions; } /** * This is the setter method for the instance variable {@link #maxVersion}. * * @param _maxVersion the maxVersion to set */ public void setMaxVersion(final Long _maxVersion) { this.maxVersion = _maxVersion; } /** * This is the getter method for the instance variable {@link #maxVersion}. * * @return value of instance variable {@link #maxVersion} */ public Long getMaxVersion() { return this.maxVersion; } /** * Setter method for instance variable {@link #rootPackageName}. * * @param _rootPackageName value for instance variable * {@link #rootPackageName} */ public void setRootPackageName(final String _rootPackageName) { this.rootPackageName = _rootPackageName; } /** * Getter method for instance variable {@link #rootPackageName}. * * @return value of instance variable {@link #rootPackageName} */ public String getRootPackageName() { return this.rootPackageName; } /** * Returns a string representation with values of all instance variables. * * @return string representation of this Application */ @Override public String toString() { return new ToStringBuilder(this) .append("application", this.application) .append("dependencies", this.dependencies) .append("versions", this.versions) .append("install", this.install) .toString(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy