1 /*
  2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 package org.openjdk.skara.bots.hgbridge;
 24 
 25 import org.openjdk.skara.bot.*;
 26 import org.openjdk.skara.vcs.Repository;
 27 
 28 import java.io.*;
 29 import java.net.URLEncoder;
 30 import java.nio.charset.StandardCharsets;
 31 import java.nio.file.*;
 32 import java.util.List;
 33 import java.util.logging.Logger;
 34 
 35 public class JBridgeBot implements Bot, WorkItem {
 36     private final ExporterConfig exporterConfig;
 37     private final Path storage;
 38     private final Logger log = Logger.getLogger("org.openjdk.bots.hgbridge");
 39 
 40     JBridgeBot(ExporterConfig exporterConfig, Path storage) {
 41         this.exporterConfig = exporterConfig;
 42         this.storage = storage.resolve(URLEncoder.encode(exporterConfig.source().toString(), StandardCharsets.UTF_8));
 43     }
 44 
 45     @Override
 46     public String toString() {
 47         return "JBridgeBot@" + exporterConfig.source();
 48     }
 49 
 50     @Override
 51     public List<WorkItem> getPeriodicItems() {
 52         return List.of(this);
 53     }
 54 
 55     @Override
 56     public boolean concurrentWith(WorkItem other) {
 57         if (other instanceof JBridgeBot) {
 58             JBridgeBot otherBridgeBot = (JBridgeBot)other;
 59             return !exporterConfig.source().equals(otherBridgeBot.exporterConfig.source());
 60         } else {
 61             return true;
 62         }
 63     }
 64 
 65     private void pushMarks(Path markSource, String destName, Path markScratchPath) throws IOException {
 66         var marksRepo = Repository.materialize(markScratchPath, exporterConfig.marksRepo().url(), exporterConfig.marksRef());
 67 
 68         // We should never change existing marks
 69         var markDest = markScratchPath.resolve(destName);
 70         var updated = Files.readString(markSource);
 71         if (Files.exists(markDest)) {
 72             var existing = Files.readString(markDest);
 73 
 74             if (!updated.startsWith(existing)) {
 75                 throw new RuntimeException("Update containing conflicting marks!");
 76             }
 77             if (existing.equals(updated)) {
 78                 // Nothing new to push
 79                 return;
 80             }
 81         } else {
 82             if (!Files.exists(markDest.getParent())) {
 83                 Files.createDirectories(markDest.getParent());
 84             }
 85         }
 86 
 87         Files.writeString(markDest, updated, StandardCharsets.UTF_8);
 88         marksRepo.add(markDest);
 89         var hash = marksRepo.commit("Updated marks", exporterConfig.marksAuthorName(), exporterConfig.marksAuthorEmail());
 90         marksRepo.push(hash, exporterConfig.marksRepo().url(), exporterConfig.marksRef());
 91     }
 92 
 93     @Override
 94     public void run(Path scratchPath) {
 95         log.fine("Running export for " + exporterConfig.source().toString());
 96 
 97         try {
 98             var converter = exporterConfig.resolve(scratchPath.resolve("converter"));
 99             var marksFile = scratchPath.resolve("marks.txt");
100             var exported = Exporter.export(converter, exporterConfig.source(), storage, marksFile);
101 
102             // Push updated marks - other marks files may be updated concurrently, so try a few times
103             var retryCount = 0;
104             while (exported.isPresent()) {
105                 try {
106                     pushMarks(marksFile,
107                               exporterConfig.source().getHost() + "/" + exporterConfig.source().getPath() + "/marks.txt",
108                               scratchPath.resolve("markspush"));
109                     break;
110                 } catch (IOException e) {
111                     retryCount++;
112                     if (retryCount > 10) {
113                         log.warning("Retry count exceeded for pushing marks");
114                         throw new UncheckedIOException(e);
115                     }
116                 }
117             }
118 
119             IOException lastException = null;
120             for (var destination : exporterConfig.destinations()) {
121                 var markerBase = destination.url().getHost() + "/" + destination.name();
122                 var successfulPushMarker = storage.resolve(URLEncoder.encode(markerBase, StandardCharsets.UTF_8) + ".success.txt");
123                 if (exported.isPresent() || !successfulPushMarker.toFile().isFile()) {
124                     var repo = exported.orElse(Exporter.current(storage).orElseThrow());
125                     try {
126                         Files.deleteIfExists(successfulPushMarker);
127                         repo.pushAll(destination.url());
128                         storage.resolve(successfulPushMarker).toFile().createNewFile();
129                     } catch (IOException e) {
130                         log.severe("Failed to push to " + destination.url());
131                         log.throwing("JBridgeBot", "run", e);
132                         lastException = e;
133                     }
134                 } else {
135                     log.fine("No changes detected in " + exporterConfig.source() + " - skipping push to " + destination.name());
136                 }
137             }
138             if (lastException != null) {
139                 throw new UncheckedIOException(lastException);
140             }
141         } catch (IOException e) {
142             throw new UncheckedIOException(e);
143         }
144     }
145 }