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(),
 67                                                "+" + exporterConfig.marksRef() + ":hgbridge_marks");
 68 
 69         // We should never change existing marks
 70         var markDest = markScratchPath.resolve(destName);
 71         var updated = Files.readString(markSource);
 72         if (Files.exists(markDest)) {
 73             var existing = Files.readString(markDest);
 74 
 75             if (!updated.startsWith(existing)) {
 76                 throw new RuntimeException("Update containing conflicting marks!");
 77             }
 78             if (existing.equals(updated)) {
 79                 // Nothing new to push
 80                 return;
 81             }
 82         } else {
 83             if (!Files.exists(markDest.getParent())) {
 84                 Files.createDirectories(markDest.getParent());
 85             }
 86         }
 87 
 88         Files.writeString(markDest, updated, StandardCharsets.UTF_8);
 89         marksRepo.add(markDest);
 90         var hash = marksRepo.commit("Updated marks", exporterConfig.marksAuthorName(), exporterConfig.marksAuthorEmail());
 91         marksRepo.push(hash, exporterConfig.marksRepo().url(), exporterConfig.marksRef());
 92     }
 93 
 94     @Override
 95     public void run(Path scratchPath) {
 96         log.fine("Running export for " + exporterConfig.source().toString());
 97 
 98         try {
 99             var converter = exporterConfig.resolve(scratchPath.resolve("converter"));
100             var marksFile = scratchPath.resolve("marks.txt");
101             var exported = Exporter.export(converter, exporterConfig.source(), storage, marksFile);
102 
103             // Push updated marks - other marks files may be updated concurrently, so try a few times
104             var retryCount = 0;
105             while (exported.isPresent()) {
106                 try {
107                     pushMarks(marksFile,
108                               exporterConfig.source().getHost() + "/" + exporterConfig.source().getPath() + "/marks.txt",
109                               scratchPath.resolve("markspush"));
110                     break;
111                 } catch (IOException e) {
112                     retryCount++;
113                     if (retryCount > 10) {
114                         log.warning("Retry count exceeded for pushing marks");
115                         throw new UncheckedIOException(e);
116                     }
117                 }
118             }
119 
120             IOException lastException = null;
121             for (var destination : exporterConfig.destinations()) {
122                 var markerBase = destination.url().getHost() + "/" + destination.name();
123                 var successfulPushMarker = storage.resolve(URLEncoder.encode(markerBase, StandardCharsets.UTF_8) + ".success.txt");
124                 if (exported.isPresent() || !successfulPushMarker.toFile().isFile()) {
125                     var repo = exported.orElse(Exporter.current(storage).orElseThrow());
126                     try {
127                         Files.deleteIfExists(successfulPushMarker);
128                         repo.pushAll(destination.url());
129                         storage.resolve(successfulPushMarker).toFile().createNewFile();
130                     } catch (IOException e) {
131                         log.severe("Failed to push to " + destination.url());
132                         log.throwing("JBridgeBot", "run", e);
133                         lastException = e;
134                     }
135                 } else {
136                     log.fine("No changes detected in " + exporterConfig.source() + " - skipping push to " + destination.name());
137                 }
138             }
139             if (lastException != null) {
140                 throw new UncheckedIOException(lastException);
141             }
142         } catch (IOException e) {
143             throw new UncheckedIOException(e);
144         }
145     }
146 }