    001/*
    002 * Copyright 2025 The Apache Software Foundation.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *      http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    013 * See the License for the specific language governing permissions and
    014 * limitations under the License.
    015 */
    016package org.apache.wiki;
    017
    018import java.io.IOException;
    019import java.io.InputStream;
    020import java.io.InputStreamReader;
    021import java.net.URL;
    022import java.net.URLConnection;
    023import java.util.Date;
    024import java.util.Properties;
    025import java.util.concurrent.ScheduledThreadPoolExecutor;
    026import java.util.concurrent.TimeUnit;
    027import org.apache.logging.log4j.LogManager;
    028import org.apache.logging.log4j.Logger;
    029import org.apache.wiki.api.Release;
    030import tools.jackson.databind.JsonNode;
    031import tools.jackson.databind.ObjectMapper;
    032
    033/**
    034 * At server start up and once a day (if enabled), this component will check for
    035 * an updated WAR file via maven central's APIs. The notification of the update
    036 * will then be disabled to users with admin permissions near the user
    037 * preference/login/out menu.
    038 *
    039 * It's usage does require internet access, so if you're not internet connected,
    040 * or cannot reach search.maven.org it won't do you much good. It can be
    041 * disabled via jspwiki properties.
    042 *
    043 * @since 3.0.0
    044 */
    045public final class ProductUpdateChecker implements Runnable {
    046
    047    /*public static void main(String[] args) throws InterruptedException {
    048        Properties p = new Properties();
    049        p.setProperty("jspwiki.updateCheck.enabled", "true");
    050        ProductUpdateChecker.initialize(p);
    051        Thread.sleep(9999999);
    052    }*/
    053    private static final Logger LOG = LogManager.getLogger(ProductUpdateChecker.class);
    054
    055    private ProductUpdateChecker(Properties props) {
    056        if ("true".equalsIgnoreCase(props.getProperty("jspwiki.updateCheck.enabled", "false"))) {
    057            int period = Integer.parseInt(props.getProperty("jspwiki.updateCheck.periodHours", "24"));
    058            if (period <= 0) {
    059                LOG.info("Configuration warning, product update checker is set for non positive value. Update check will only be performed at server start up. Set jspwiki.updateCheck.periodHours to a positive value to enable periodic checks.");
    060                pool.execute(this);
    061            } else {
    062                pool = new ScheduledThreadPoolExecutor(1);
    063                pool.scheduleAtFixedRate(this, 0, 1, TimeUnit.DAYS);
    064            }
    065        } else {
    066            LOG.info("Product update checker is disabled");
    067        }
    068    }
    069    private ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
    070
    071    public static void initialize(Properties props) {
    072        INSTANCE = new ProductUpdateChecker(props);
    073    }
    074    private static ProductUpdateChecker INSTANCE = null;
    075
    076    /**
    077     * if the service is disabled, this will return null
    078     *
    079     * @return
    080     */
    081    public static synchronized ProductUpdateChecker getInstance() {
    082        return INSTANCE;
    083    }
    084
    085    @Override
    086    public void run() {
    087        if (Status.UPDATE_AVAILABLE == status.isUpToDate) {
    088            //don't bother checking again. if it's updated, we'll have to restart anyhow
    089            return;
    090        }
    091        LOG.debug("checking for product updates");
    092        //option a) xml
    093        //https://repo1.maven.org/maven2/org/apache/jspwiki/jspwiki-war/maven-metadata.xml
    094
    095        //option b) json
    096        URLConnection openConnection = null;
    097        InputStream inputStream = null;
    098        InputStreamReader reader = null;
    099        try {
    100            URL url = new URL("https://search.maven.org/solrsearch/select?q=g:%22org.apache.jspwiki%22+AND+a:%22jspwiki-war%22&wt=json");
    101            openConnection = url.openConnection();
    102            openConnection.connect();
    103            inputStream = openConnection.getInputStream();
    104            ObjectMapper mapper = new ObjectMapper();
    105            JsonNode readTree = mapper.readTree(inputStream);
    106            readTree = readTree.get("response").get("docs").get(0);
    107
    108            String latestVersion = readTree.get("latestVersion").asString();
    109            long releaseTimestamp = readTree.get("timestamp").asLong();
    110            status.timeStampOfLastCheck = System.currentTimeMillis();
    111            status.lastestVersionReleaseDate = releaseTimestamp;
    112            status.latestVersion = latestVersion;
    113            if ((Release.VERSION + "." + Release.REVISION + "." + Release.MINORREVISION).equals(latestVersion)) {
    114                status.isUpToDate = Status.UP_TO_DATE;
    115            } else {
    116                status.isUpToDate = Status.UPDATE_AVAILABLE;
    117            }
    118            if (status.isUpToDate == Status.UPDATE_AVAILABLE) {
    119                LOG.info("There is a JSPWiki update available. Current version is " + Release.VERSION + "." + Release.REVISION + "." + Release.MINORREVISION +
    120                        " update version is " + status.latestVersion + " which was released " + new Date(status.lastestVersionReleaseDate).toString());
    121            }
    122        } catch (Exception ex) {
    123            status.isUpToDate = Status.UNKNOWN;
    124            status.timeStampOfLastCheck = System.currentTimeMillis();
    125            LOG.warn("product update check failed " + ex.getMessage(), ex);
    126        } finally {
    127            if (reader != null) {
    128                try {
    129                    reader.close();
    130                } catch (IOException ex) {
    131                    LOG.debug(ex.getMessage());
    132                }
    133            }
    134            if (inputStream != null) {
    135                try {
    136                    inputStream.close();
    137                } catch (IOException ex) {
    138                    LOG.debug(ex.getMessage());
    139                }
    140            }
    141
    142        }
    143
    144    }
    145    private final UpdateStatus status = new UpdateStatus();
    146
    147    public UpdateStatus getUpdateStatus() {
    148        return status;
    149
    150    }
    151
    152    public enum Status {
    153        UNKNOWN,
    154        UP_TO_DATE,
    155        UPDATE_AVAILABLE
    156    }
    157
    158    public static class UpdateStatus {
    159
    160        private Status isUpToDate = Status.UNKNOWN;
    161        private String latestVersion = null;
    162        private long lastestVersionReleaseDate = Long.MIN_VALUE;
    163        ;
    164        private long timeStampOfLastCheck = Long.MIN_VALUE;
    165
    166        public Status getIsUpToDate() {
    167            return isUpToDate;
    168        }
    169
    170        public String getLatestVersion() {
    171            return latestVersion;
    172        }
    173
    174        public long getLastestVersionReleaseDate() {
    175            return lastestVersionReleaseDate;
    176        }
    177
    178        public long getTimeStampOfLastCheck() {
    179            return timeStampOfLastCheck;
    180        }
    181
    182    }
    183
    184    public void shutdown() {
    185        if (pool != null) {
    186            pool.shutdown();
    187            pool.shutdownNow();
    188            pool = null;
    189        }
    190    }
    191
    192}