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}