001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 *  Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *     or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014
015package ch.qos.logback.core.util;
016
017import ch.qos.logback.core.Context;
018import ch.qos.logback.core.status.InfoStatus;
019import ch.qos.logback.core.status.WarnStatus;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.lang.module.ModuleDescriptor;
024import java.util.Optional;
025import java.util.Properties;
026
027import static ch.qos.logback.core.CoreConstants.NA;
028
029
030/**
031 * Utility class for handling and validating version information of various artifacts.
032 *
033 * <p>It is used by logback-classic, logback-access-common, logback-access-jetty11, logback-access-tomcat, etc.
034 * to alert users about version discrepancies between dependent and dependee artifacts.
035 * </p>
036 *
037 * @since 1.5.25
038 */
039public class VersionUtil {
040
041    /**
042     * Retrieves the version of an artifact, such as logback-core.jar, logback-access-common.jar etc.
043     *
044     * <p>The aClass parameter is assumed to be part of the artifact.
045     * </p>
046     *
047     * <p>The method first attempts to get the version from the module information. If the module version
048     * is not available, it falls back to retrieving the implementation version from the package.
049     * </p>
050     *
051     * @param aClass the class from which to retrieve the version information
052     * @return the version of the artifact where aClass is found, or null if the version cannot be determined
053     */
054    static public String getVersionOfArtifact(Class<?> aClass) {
055        String moduleVersion = getVersionOfClassByModule(aClass);
056        if (moduleVersion != null)
057            return moduleVersion;
058
059        Package pkg = aClass.getPackage();
060        if (pkg == null) {
061            return null;
062        }
063        return pkg.getImplementationVersion();
064    }
065
066    static public String nonNull(String input) {
067        if (input == null) {
068            return NA;
069        } else {
070            return input;
071        }
072    }
073
074    /**
075     * Retrieves the version of an artifact from the artifact's module metadata.
076     *
077     * <p>If the module or its descriptor does not provide a version, the method returns null.
078     * </p>
079     *
080     * @param aClass a class from which to retrieve the version information
081     * @return the version of class' module as a string, or null if the version cannot be determined
082     */
083    static private String getVersionOfClassByModule(Class<?> aClass) {
084        Module module = aClass.getModule();
085        if (module == null)
086            return null;
087
088        ModuleDescriptor md = module.getDescriptor();
089        if (md == null)
090            return null;
091        Optional<String> opt = md.rawVersion();
092        return opt.orElse(null);
093    }
094
095    /**
096     * Retrieves the version of a module using a properties file associated with the module.
097     *
098     * <p>Unfortunately, this code cannot be called by other modules. It needs to be duplicated.</p>
099     *
100     * <p>The method looks for a properties file with a name derived from the <code>moduleName</code> parameter,
101     * in the same location, e.g. package, as the <code>aClass</code> parameter. It attempts to load the properties file
102     * and fetch the version information using a specific key.
103     * </p>
104     *
105     * <p>The properties file is expected to be in the same package as the class provided, and named
106     * <code>moduleName-version.properties</code>. The properties file should contain a single key-value pair,
107     * where the key is <code>moduleName-version</code>, and the value is the module version.
108     *
109     * @param aClass     the class used to locate the resource file, the properties file is expected to be in the same package
110     * @param moduleName the name of the module, which is used to construct the properties file name and the key
111     * @return the version of the module as a string, or null if the version cannot be determined
112     * @since 1.5.26
113     */
114    static public String getArtifactVersionBySelfDeclaredProperties(Class<?> aClass, String moduleName) {
115        Properties props = new Properties();
116        // example propertiesFileName: logback-core-version.properties
117        //
118        String propertiesFileName = moduleName + "-version.properties";
119        String propertyKey = moduleName + "-version";
120        try (InputStream is = aClass.getResourceAsStream(propertiesFileName)) {
121            if (is != null) {
122                props.load(is);
123                return props.getProperty(propertyKey);
124            } else {
125                return null;
126            }
127        } catch (IOException e) {
128            return null;
129        }
130    }
131
132
133    // dependency synonym dependee
134    // depender synonym dependent
135
136    static String getExpectedVersionOfDependencyByProperties(Class<?> dependerClass, String propertiesFileName, String dependencyNameAsKey) {
137        Properties props = new Properties();
138        // propertiesFileName : logback-access-common-dependees.properties
139        try (InputStream is = dependerClass.getClassLoader()
140                .getResourceAsStream(propertiesFileName)) {
141            if (is != null) {
142                props.load(is);
143                return props.getProperty(dependencyNameAsKey);
144            } else {
145                return null;
146            }
147        } catch (IOException e) {
148            return null;
149        }
150    }
151
152
153    static public void checkForVersionEquality(Context context, Class<?> dependerClass, Class<?> dependencyClass, String dependentName, String dependencyName) {
154        // the depender depends on the dependency
155        String dependerVersion = nonNull(getVersionOfArtifact(dependerClass));
156        String dependencyVersion = nonNull(getVersionOfArtifact(dependencyClass));
157
158        checkForVersionEquality(context, dependerVersion, dependencyVersion, dependentName, dependencyName);
159    }
160
161    // depender depends on dependency
162    // dependency synonym dependee
163    // depender synonym dependent
164    static public void checkForVersionEquality(Context context, Class<?> dependerClass, String dependencyVersion, String dependentName, String dependencyName) {
165        String dependerVersion = nonNull(getVersionOfArtifact(dependerClass));
166        checkForVersionEquality(context, dependerVersion, dependencyVersion, dependentName, dependencyName);
167    }
168
169
170    /**
171     * Compares the versions of a dependent and a dependency to determine if they are equal.
172     * Updates the context's status manager with version information and logs a warning if the versions differ.
173     *
174     * @param context           the logging context to which status messages are added
175     * @param dependentVersion  the version string of the dependent component
176     * @param dependencyVersion the version string of the dependency component
177     * @param dependentName     the name of the dependent component
178     * @param dependencyName    the name of the dependency component
179     * @since 1.5.26
180     */
181    static public void checkForVersionEquality(Context context, String dependentVersion, String dependencyVersion, String dependentName, String dependencyName) {
182        // the dependent depends on the dependency
183        addFoundVersionStatus(context, dependentName, dependentVersion);
184
185        if (dependentVersion.equals(NA) || !dependentVersion.equals(dependencyVersion)) {
186            addFoundVersionStatus(context, dependencyName, dependencyVersion);
187            String discrepancyMsg = String.format("Versions of %s and %s are different!", dependencyName, dependentName);
188            context.getStatusManager().add(new WarnStatus(discrepancyMsg, context));
189        }
190    }
191
192
193
194
195
196        private static void addFoundVersionStatus(Context context, String name, String version) {
197        String foundDependent = String.format("Found %s version %s", name, version);
198        context.getStatusManager().add(new InfoStatus(foundDependent, context));
199    }
200
201
202    private static String nameToFilename(String name) {
203        return name + "-dependencies.properties";
204    }
205
206//    // dependency synonym dependee
207//    // depender synonym dependent
208//    static public void compareExpectedAndFoundVersion(Context context, Class<?> dependerClass, Class<?> dependencyClass,
209//                                                      String dependerName, String dependencyName) {
210//        String actualDependencyVersion = nonNull(getVersionOfArtifact(dependencyClass));
211//        String dependerVersion = nonNull(getVersionOfArtifact(dependerClass));
212//
213//        compareExpectedAndFoundVersion(context, actualDependencyVersion, dependerClass, dependerVersion, dependerName, dependencyName);
214//    }
215
216    // dependency synonym dependee
217    // depender synonym dependent
218    static public void compareExpectedAndFoundVersion(Context context, String actualDependencyVersion, Class<?>dependerClass, String dependerVersion,
219                                                      String dependerName, String dependencyName) {
220
221        String expectedDependencyVersion = nonNull(getExpectedVersionOfDependencyByProperties(dependerClass, nameToFilename(dependerName), dependencyName));
222
223        addFoundVersionStatus(context, dependencyName, actualDependencyVersion);
224        addFoundVersionStatus(context, dependerName, dependerVersion);
225
226        if (!expectedDependencyVersion.equals(actualDependencyVersion)) {
227            String discrepancyMsg = String.format("Expected version of %s is %s but found %s", dependencyName, expectedDependencyVersion, actualDependencyVersion);
228            context.getStatusManager().add(new WarnStatus(discrepancyMsg, context));
229        }
230    }
231}