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 v2.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// depender depends on dependency
030
031// dependency synonym dependee (only use dependency)
032// depender synonym dependent (only use depender)
033
034/**
035 * Utility class for handling and validating version information of various artifacts.
036 *
037 * <p>It is used by logback-classic, logback-access-common, logback-access-jetty11, logback-access-tomcat, etc.
038 * to alert users about version discrepancies between depender and dependency artifacts.
039 * </p>
040 *
041 * @since 1.5.25
042 */
043public class VersionUtil {
044
045    /**
046     * Retrieves the version of an artifact, such as logback-core.jar, logback-access-common.jar etc.
047     *
048     * <p>The aClass parameter is assumed to be part of the artifact.
049     * </p>
050     *
051     * <p>The method first attempts to get the version from the module information. If the module version
052     * is not available, it falls back to retrieving the implementation version from the package.
053     * </p>
054     *
055     * @param aClass the class from which to retrieve the version information
056     * @return the version of the artifact where aClass is found, or null if the version cannot be determined
057     * @deprecated
058     */
059    static public String getVersionOfArtifact(Class<?> aClass) {
060        String moduleVersion = getVersionOfClassByModule(aClass);
061        if (moduleVersion != null)
062            return moduleVersion;
063
064        Package pkg = aClass.getPackage();
065        if (pkg == null) {
066            return null;
067        }
068        return pkg.getImplementationVersion();
069    }
070
071    static public String nonNull(String input) {
072        if (input == null) {
073            return NA;
074        } else {
075            return input;
076        }
077    }
078
079    /**
080     * Retrieves the version of an artifact from the artifact's module metadata.
081     *
082     * <p>If the module or its descriptor does not provide a version, the method returns null.
083     * </p>
084     *
085     * @param aClass a class from which to retrieve the version information
086     * @return the version of class' module as a string, or null if the version cannot be determined
087     */
088    static private String getVersionOfClassByModule(Class<?> aClass) {
089        Module module = aClass.getModule();
090        if (module == null)
091            return null;
092
093        ModuleDescriptor md = module.getDescriptor();
094        if (md == null)
095            return null;
096        Optional<String> opt = md.rawVersion();
097        return opt.orElse(null);
098    }
099
100    /**
101     * Retrieves the version of a module using a properties file associated with the module.
102     *
103     * <p>Unfortunately, this code cannot be called by other modules. It needs to be duplicated.</p>
104     *
105     * <p>The method looks for a properties file with a name derived from the <code>moduleName</code> parameter,
106     * in the same location, e.g. package, as the <code>aClass</code> parameter. It attempts to load the properties file
107     * and fetch the version information using a specific key.
108     * </p>
109     *
110     * <p>The properties file is expected to be in the same package as the class provided, and named
111     * <code>moduleName-version.properties</code>. The properties file should contain a single key-value pair,
112     * where the key is <code>moduleName-version</code>, and the value is the module version.
113     *
114     * @param aClass     the class used to locate the resource file, the properties file is expected to be in the same package
115     * @param moduleName the name of the module, which is used to construct the properties file name and the key
116     * @return the version of the module as a string, or null if the version cannot be determined
117     * @since 1.5.26
118     * @deprecated (this code cannot be shared and is useless here)
119     */
120    static public String getArtifactVersionBySelfDeclaredProperties(Class<?> aClass, String moduleName) {
121        Properties props = new Properties();
122        // example propertiesFileName: logback-core-version.properties
123        //
124        String propertiesFileName = moduleName + "-version.properties";
125        String propertyKey = moduleName + "-version";
126        try (InputStream is = aClass.getResourceAsStream(propertiesFileName)) {
127            if (is != null) {
128                props.load(is);
129                return props.getProperty(propertyKey);
130            } else {
131                return null;
132            }
133        } catch (IOException e) {
134            return null;
135        }
136    }
137
138
139
140
141    static String getExpectedVersionOfDependencyByProperties(Class<?> dependerClass, String propertiesFileName, String dependencyNameAsKey) {
142        Properties props = new Properties();
143        // propertiesFileName : logback-access-common-dependencies.properties
144        try (InputStream is = dependerClass.getClassLoader()
145                .getResourceAsStream(propertiesFileName)) {
146            if (is != null) {
147                props.load(is);
148                return props.getProperty(dependencyNameAsKey);
149            } else {
150                return null;
151            }
152        } catch (IOException e) {
153            return null;
154        }
155    }
156
157
158    static public void checkForVersionEquality(Context context, Class<?> dependerClass, Class<?> dependencyClass, String dependerName, String dependencyName) {
159        // the depender depends on the dependency
160        String dependerVersion = nonNull(getVersionOfArtifact(dependerClass));
161        String dependencyVersion = nonNull(getVersionOfArtifact(dependencyClass));
162
163        checkForVersionEquality(context, dependerVersion, dependencyVersion, dependerName, dependencyName);
164    }
165
166    static public void checkForVersionEquality(Context context, Class<?> dependerClass, String dependencyVersion, String dependerName, String dependencyName) {
167        String dependerVersion = nonNull(getVersionOfArtifact(dependerClass));
168        checkForVersionEquality(context, dependerVersion, dependencyVersion, dependerName, dependencyName);
169    }
170
171
172    /**
173     * Compares the versions of a depender and a dependency to determine if they are equal.
174     * Updates the context's status manager with version information and logs a warning
175     * if the versions differ.
176     *
177     * @since 1.5.26
178     */
179    static public void checkForVersionEquality(Context context, String dependerVersion, String dependencyVersion, String dependerName, String dependencyName) {
180        // the depender depends on the dependency
181        addFoundVersionStatus(context, dependerName, dependerVersion);
182
183        dependerVersion = nonNull(dependerVersion);
184
185        if (dependerVersion.equals(NA) || !dependerVersion.equals(dependencyVersion)) {
186            addFoundVersionStatus(context, dependencyName, dependencyVersion);
187            String discrepancyMsg = String.format("Versions of %s and %s are different or unknown.", dependencyName, dependerVersion);
188            context.getStatusManager().add(new WarnStatus(discrepancyMsg, context));
189        }
190    }
191
192
193    private static void addFoundVersionStatus(Context context, String name, String version) {
194        String foundDependent = String.format("Found %s version %s", name, nonNull(version));
195        context.getStatusManager().add(new InfoStatus(foundDependent, context));
196    }
197
198    private static String nameToPropertiesFilename(String name) {
199        return name + "-dependencies.properties";
200    }
201
202    /**
203     * Compares the expected version of a dependency with the actual version found and updates the status context.
204     * If the versions do not match, a warning is added to the context's status manager.
205     *
206     * <p>Note: This method is used be logback-access-jetty11/12 and logback-access-tomcat.</p>
207     *
208     */
209    static public void compareExpectedAndFoundVersion(Context context, String actualDependencyVersion, Class<?> dependerClass, String dependerVersion,
210                                                      String dependerName, String dependencyName) {
211
212        String expectedDependencyVersion = nonNull(getExpectedVersionOfDependencyByProperties(dependerClass, nameToPropertiesFilename(dependerName), dependencyName));
213
214        addFoundVersionStatus(context, dependencyName, actualDependencyVersion);
215        addFoundVersionStatus(context, dependerName, dependerVersion);
216
217        if (!expectedDependencyVersion.equals(actualDependencyVersion)) {
218            String discrepancyMsg = String.format("Expected version of %s is %s but found %s", dependencyName, expectedDependencyVersion, actualDependencyVersion);
219            context.getStatusManager().add(new WarnStatus(discrepancyMsg, context));
220        }
221    }
222}