From 54781d6222693241318709960ef1f07895e26e3e Mon Sep 17 00:00:00 2001 From: Nikita Smirnov <46124551+Nikita-Smirnov-Exactpro@users.noreply.github.com> Date: Tue, 3 Oct 2023 17:17:15 +0400 Subject: [PATCH] Added auto book feature (#2) Co-authored-by: Oleg Smirnov --- cradle-admin-tool-http/README.md | 13 +- .../th2/cradle/adm/http/Application.java | 1 + .../adm/http/AutoPageConfiguration.java | 13 +- .../th2/cradle/adm/http/Configuration.java | 11 +- .../th2/cradle/adm/http/PageManager.java | 115 ++++++++++++++--- .../th2/cradle/adm/http/PageManagerTest.java | 119 ++++++++++++++++-- gradle.properties | 2 +- 7 files changed, 236 insertions(+), 38 deletions(-) diff --git a/cradle-admin-tool-http/README.md b/cradle-admin-tool-http/README.md index 7cd2a60..a1b982c 100644 --- a/cradle-admin-tool-http/README.md +++ b/cradle-admin-tool-http/README.md @@ -1,4 +1,4 @@ -# cradle-admin-tool-http (1.7.2) +# cradle-admin-tool-http (1.8.0) Service which allows user to manage books/pages via RestAPI requests. - The first page in a book can be created only if start time is more than current time. - After the first page all new pages must have start time more than current time + `bookRefreshIntervalMillis` * 2 @@ -12,7 +12,10 @@ Service which allows user to manage books/pages via RestAPI requests. - **ip** - host where http cradle admin instance will be instanciated. Default value: `0.0.0.0` - **port** - port on which http server will listen user requests. Default value: `8080` - **page-recheck-interval** - interval in seconds which `PageManager` service checks if new page is required to create or not based on duration values presented in `auto-pages`. Default value: 60 seconds -- **auto-pages** - defines rule for automatic pages creation for multiple books. If empty no pages will be created automatically. Default value: `empty_map`. +- **auto-book** - if `true` than cradle-admin-tool creates books with first page for each value from `auto-pages` option when target book doesn't exist in cradle. + Creation book time is calculate by the `current time - 1 day` formula to cover events and messages published a bit earlier than cradle-admin-tool started. Default value: `true`. + Please note you can create your own book via REST API later. +- **auto-pages** - defines rule for automatic pages creation for multiple books. If empty no pages will be created automatically. - **page-duration** - defines duration of the page for the book. Value uses the Java Duration format. You can read more about it [here](https://docs.oracle.com/javase/8/docsT/api/java/time/Duration.html#parse-java.lang.CharSequence-). - **page-start-time** - baseline date and time for every new page created by `PageManager` for this book. @@ -29,6 +32,7 @@ spec: custom-config: ip: 198.168.0.2 port: 8080 + auto-book: true auto-pages: book1: page-duration: PT60S @@ -44,6 +48,11 @@ spec: ## Release notes +### 1.8.0 + ++ Feature: + + Added auto-book functionality + ### 1.7.2 + Bug fix: diff --git a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Application.java b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Application.java index 520bdff..d30b019 100644 --- a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Application.java +++ b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Application.java @@ -54,6 +54,7 @@ public static void main(String[] args) { resources.add( new PageManager( storage, + config.isAutoBook(), config.getAutoPages(), config.getPageRecheckInterval(), settings.calculatePageActionRejectionThreshold() * 2 diff --git a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/AutoPageConfiguration.java b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/AutoPageConfiguration.java index 9a7555b..e345750 100644 --- a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/AutoPageConfiguration.java +++ b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/AutoPageConfiguration.java @@ -1,4 +1,4 @@ -/******************************************************************************* +/* * Copyright 2023 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,10 +12,11 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - ******************************************************************************/ + */ package com.exactpro.th2.cradle.adm.http; import com.fasterxml.jackson.annotation.JsonProperty; + import java.time.Duration; import java.time.Instant; @@ -34,4 +35,12 @@ public Duration getPageDuration() { public Instant getPageStartTime() { return pageStartTime; } + + public void setPageDuration(Duration pageDuration) { + this.pageDuration = pageDuration; + } + + public void setPageStartTime(Instant pageStartTime) { + this.pageStartTime = pageStartTime; + } } diff --git a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Configuration.java b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Configuration.java index d909d5a..84cfa64 100644 --- a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Configuration.java +++ b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/Configuration.java @@ -17,6 +17,8 @@ package com.exactpro.th2.cradle.adm.http; import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Collections; import java.util.Map; @SuppressWarnings("FieldMayBeFinal") @@ -35,8 +37,11 @@ public class Configuration { @JsonProperty("page-recheck-interval") private int pageRecheckInterval = DEFAULT_PAGE_RECHECK_INTERVAL_SEC; + @JsonProperty("auto-book") + private boolean autoBook = true; + @JsonProperty("auto-pages") - private Map autoPages; + private Map autoPages = Collections.emptyMap(); public String getIp() { return ip; @@ -46,6 +51,10 @@ public int getPort() { return port; } + public boolean isAutoBook() { + return autoBook; + } + public Map getAutoPages() { return autoPages; } diff --git a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/PageManager.java b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/PageManager.java index 900f387..15d028f 100644 --- a/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/PageManager.java +++ b/cradle-admin-tool-http/src/main/java/com/exactpro/th2/cradle/adm/http/PageManager.java @@ -17,25 +17,35 @@ package com.exactpro.th2.cradle.adm.http; import com.exactpro.cradle.BookInfo; +import com.exactpro.cradle.BookListEntry; +import com.exactpro.cradle.BookToAdd; import com.exactpro.cradle.CradleStorage; import com.exactpro.cradle.PageInfo; import com.exactpro.cradle.utils.CradleStorageException; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static org.apache.commons.lang3.StringUtils.lowerCase; +import static org.apache.commons.lang3.StringUtils.trim; public class PageManager implements AutoCloseable, Runnable{ - private static final Logger logger = LoggerFactory.getLogger(Application.class); - private static final String AUTO_PAGE_COMMENT = "auto-page"; + private static final Logger LOGGER = LoggerFactory.getLogger(PageManager.class); + static final String AUTO_PAGE_COMMENT = "auto-page"; + static final String AUTO_BOOK_DESCRIPTION = "auto-book"; private final CradleStorage storage; private final long pageActionRejectionThreshold; @@ -44,12 +54,13 @@ public class PageManager implements AutoCloseable, Runnable{ public PageManager( CradleStorage storage, + boolean autoBooks, Map autoPages, int pageRecheckInterval, long pageActionRejectionThreshold - ) throws CradleStorageException { + ) { if (autoPages == null || autoPages.isEmpty()) { - logger.info("auto-page configuration is not provided, pages will not be generated automatically"); + LOGGER.info("auto-page configuration is not provided, pages will not be generated automatically"); this.storage = null; this.pageActionRejectionThreshold = 0; this.executorService = null; @@ -57,43 +68,64 @@ public PageManager( return; } - this.storage = storage; - this.pageActionRejectionThreshold = pageActionRejectionThreshold; + Map normalisedBookName = autoPages.entrySet().stream() + .collect(Collectors.toMap( + entry -> normaliseBookName(entry.getKey()), + Map.Entry::getValue + )); - books = new HashMap<>(); - for (String bookName : autoPages.keySet()) { - books.put(bookName, new AutoPageInfo(autoPages.get(bookName), storage.refreshBook(bookName))); + if (normalisedBookName.size() != autoPages.size()) { + throw new IllegalArgumentException("Some of books have the same name after normalization" + + ", origin: " + autoPages.keySet() + + ", normalized: " + normalisedBookName.keySet()); } - logger.info("Managing pages for books {} every {} sec", books.keySet().toArray(), pageRecheckInterval); + books = normalisedBookName.entrySet().stream() + .collect(Collectors.toUnmodifiableMap( + Map.Entry::getKey, + entry -> new AutoPageInfo(entry.getValue(), getOrCreateBook(storage, entry.getKey(), autoBooks)) + )); + + this.storage = storage; + this.pageActionRejectionThreshold = pageActionRejectionThreshold; + + LOGGER.info("Managing pages for books {} every {} sec", books.keySet().toArray(), pageRecheckInterval); executorService = Executors.newScheduledThreadPool(1); executorService.scheduleAtFixedRate(this, 0, pageRecheckInterval, TimeUnit.SECONDS); } + @NotNull + private static String normaliseBookName(String origin) { + String bookName = lowerCase(trim(origin)); + if (bookName == null || bookName.isEmpty()) { + throw new IllegalArgumentException("One of book is null or empty"); + } + return bookName; + } + private BookInfo checkBook(BookInfo book, AutoPageConfiguration autoPageConfiguration) throws Exception { Instant pageStartBase = autoPageConfiguration.getPageStartTime(); Duration pageDuration = autoPageConfiguration.getPageDuration(); Instant nowPlusThreshold = Instant.now().plusMillis(pageActionRejectionThreshold); - long nowMillis = nowPlusThreshold.toEpochMilli(); PageInfo pageInfo = book.getLastPage(); if (pageInfo == null) { - return storage.addPage(book.getId(), "auto-page-" + nowMillis, nowPlusThreshold, AUTO_PAGE_COMMENT); + return createAutoPage(storage, book, nowPlusThreshold); } Instant lastPageStart = pageInfo.getStarted(); if (lastPageStart.isBefore(nowPlusThreshold)) { int comparison = nowPlusThreshold.compareTo(pageStartBase); if (comparison < 0) { - return storage.addPage(book.getId(), "auto-page-" + nowMillis, pageStartBase, AUTO_PAGE_COMMENT); + return createAutoPage(storage, book, pageStartBase); } else if (comparison > 0) { Duration diff = Duration.between(pageStartBase, nowPlusThreshold); Instant nextMark = pageStartBase.plus(pageDuration.multipliedBy(diff.dividedBy(pageDuration) + 1)); - return storage.addPage(book.getId(), "auto-page-" + nowMillis, nextMark, AUTO_PAGE_COMMENT); + return createAutoPage(storage, book, nextMark); } else { - return storage.addPage(book.getId(), "auto-page-" + nowMillis, pageStartBase.plus(pageDuration), AUTO_PAGE_COMMENT); + return createAutoPage(storage, book, pageStartBase.plus(pageDuration)); } } @@ -106,7 +138,7 @@ public void close() throws Exception { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { List tasks = executorService.shutdownNow(); - logger.warn("Executor can't stop during 5 seconds, " + tasks + " tasks that never commenced execution"); + LOGGER.warn("Executor can't stop during 5 seconds, " + tasks + " tasks that never commenced execution"); } } } @@ -117,8 +149,51 @@ public void run() { try { autoPageInfo.setBookInfo(checkBook(autoPageInfo.getBookInfo(), autoPageInfo.getAutoPageConfiguration())); } catch (Exception e) { - logger.error("Exception processing book {}", bookName, e); + LOGGER.error("Exception processing book {}", bookName, e); } }); } + + private static BookInfo getOrCreateBook(@NotNull CradleStorage storage, String bookName, boolean autoBook) { + try { + BookListEntry bookListEntry = storage.listBooks().stream() + .filter(entry -> Objects.equals(entry.getName(), bookName)) + .findFirst() + .orElse(null); + + if (bookListEntry != null) { + return storage.refreshBook(bookName); + } + + if (!autoBook) { + throw new IllegalStateException("Storage doesn't contain the '" + bookName + "' book. Create book manually or enable auto-book functionality in configuration"); + } + + BookToAdd bookToAdd = new BookToAdd( + bookName, + Instant.now().minus(1, ChronoUnit.DAYS) + ); + bookToAdd.setFullName(bookName); + bookToAdd.setDesc(AUTO_BOOK_DESCRIPTION); + + BookInfo bookInfo = storage.addBook(bookToAdd); + + LOGGER.info("Created '{}' book, time: {}, full name: {}, description: {}", + bookName, + bookToAdd.getCreated(), + bookToAdd.getFullName(), + bookToAdd.getDesc()); + + createAutoPage(storage, bookInfo, bookInfo.getCreated()); + LOGGER.info("Added first page, book: {}, time: {}", bookInfo.getId().getName(), bookInfo.getCreated()); + return bookInfo; + } catch (Exception e) { + throw new RuntimeException("Book with name '" + bookName + "' can't be created", e); + } + } + + private static BookInfo createAutoPage(CradleStorage storage, BookInfo book, Instant nowPlusThreshold) throws CradleStorageException, IOException { + long nowMillis = nowPlusThreshold.toEpochMilli(); + return storage.addPage(book.getId(), "auto-page-" + nowMillis, nowPlusThreshold, AUTO_PAGE_COMMENT); + } } diff --git a/cradle-admin-tool-http/src/test/java/com/exactpro/th2/cradle/adm/http/PageManagerTest.java b/cradle-admin-tool-http/src/test/java/com/exactpro/th2/cradle/adm/http/PageManagerTest.java index 5d776cb..7db6a2c 100644 --- a/cradle-admin-tool-http/src/test/java/com/exactpro/th2/cradle/adm/http/PageManagerTest.java +++ b/cradle-admin-tool-http/src/test/java/com/exactpro/th2/cradle/adm/http/PageManagerTest.java @@ -17,28 +17,39 @@ import com.exactpro.cradle.BookId; import com.exactpro.cradle.BookInfo; +import com.exactpro.cradle.BookListEntry; import com.exactpro.cradle.CradleStorage; import com.exactpro.cradle.PageInfo; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; public class PageManagerTest { + private static final String SCHEMA_VERSION = "test-schema-version"; + @ParameterizedTest @ValueSource(booleans = { true, false }) public void autoPage(boolean baseTimeBefore) throws Exception { @@ -47,12 +58,19 @@ public void autoPage(boolean baseTimeBefore) throws Exception { Duration duration = Duration.of(100, ChronoUnit.SECONDS); long threshold = duration.dividedBy(2).toMillis(); - BookInfo longTimeBefore = createBookInfo(now.minus(duration.multipliedBy(2)), "a long time before"); - BookInfo before = createBookInfo(now.minus(duration), "before"); - BookInfo equal = createBookInfo(now, "equal"); - BookInfo after = createBookInfo(now.plus(duration), "after"); + BookInfo longTimeBefore = createBookInfoWithPage(now.minus(duration.multipliedBy(2)), "a long time before"); + BookInfo before = createBookInfoWithPage(now.minus(duration), "before"); + BookInfo equal = createBookInfoWithPage(now, "equal"); + BookInfo after = createBookInfoWithPage(now.plus(duration), "after"); CradleStorage mockStorage = mock(CradleStorage.class); + List listEntries = List.of( + new BookListEntry(longTimeBefore.getId().getName(), SCHEMA_VERSION), + new BookListEntry(before.getId().getName(), SCHEMA_VERSION), + new BookListEntry(equal.getId().getName(), SCHEMA_VERSION), + new BookListEntry(after.getId().getName(), SCHEMA_VERSION) + ); + when(mockStorage.listBooks()).thenReturn(listEntries); when(mockStorage.refreshBook(same(longTimeBefore.getFullName()))).thenReturn(longTimeBefore); when(mockStorage.refreshBook(same(before.getId().getName()))).thenReturn(before); when(mockStorage.refreshBook(same(equal.getId().getName()))).thenReturn(equal); @@ -62,9 +80,9 @@ public void autoPage(boolean baseTimeBefore) throws Exception { when(mockStorage.addPage(same(equal.getId()), any(), any(), any())).thenReturn(equal); when(mockStorage.addPage(same(after.getId()), any(), any(), any())).thenReturn(after); - AutoPageConfiguration autoPageConfig = mock(AutoPageConfiguration.class); - when(autoPageConfig.getPageDuration()).thenReturn(duration); - when(autoPageConfig.getPageStartTime()).thenReturn(baseTimeBefore ? now.minus(duration) : now.plus(duration)); + AutoPageConfiguration autoPageConfig = new AutoPageConfiguration(); + autoPageConfig.setPageDuration(duration); + autoPageConfig.setPageStartTime(baseTimeBefore ? now.minus(duration) : now.plus(duration)); Map mapping = Map.of( @@ -73,7 +91,8 @@ public void autoPage(boolean baseTimeBefore) throws Exception { equal.getId().getName(), autoPageConfig, after.getId().getName(), autoPageConfig ); - try (PageManager ignored = new PageManager(mockStorage, mapping, 60*60*12, threshold)) { + try (PageManager ignored = new PageManager(mockStorage, false, mapping, 60*60*12, threshold)) { + verify(mockStorage, times(4)).listBooks(); verify(mockStorage).refreshBook(longTimeBefore.getId().getName()); verify(mockStorage).refreshBook(before.getId().getName()); verify(mockStorage).refreshBook(equal.getId().getName()); @@ -81,9 +100,9 @@ public void autoPage(boolean baseTimeBefore) throws Exception { verify(mockStorage, timeout(1000)) .addPage(same(longTimeBefore.getId()), any(), eq(now.plus(duration)), any()); - verify(mockStorage) + verify(mockStorage, timeout(1000)) .addPage(same(before.getId()), any(), eq(now.plus(duration)), any()); - verify(mockStorage) + verify(mockStorage, timeout(1000)) .addPage(same(equal.getId()), any(), eq(now.plus(duration)), any()); verify(mockStorage, never()) .addPage(same(after.getId()), any(), any(), any()); @@ -92,13 +111,89 @@ public void autoPage(boolean baseTimeBefore) throws Exception { } } - private BookInfo createBookInfo(Instant lastStarted, String fullName) { + @Test + public void emptyStorageAndAutoBookFalse() { + Instant now = Instant.now(); + + Duration duration = Duration.of(100, ChronoUnit.SECONDS); + long threshold = duration.dividedBy(2).toMillis(); + + CradleStorage mockStorage = mock(CradleStorage.class); + + AutoPageConfiguration autoPageConfig = new AutoPageConfiguration(); + autoPageConfig.setPageDuration(duration); + autoPageConfig.setPageStartTime(now.plus(duration)); + + String book = "test-new-book"; + Map mapping = Map.of( + book, autoPageConfig + ); + RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> new PageManager(mockStorage, false, mapping, 60*60*12, threshold).close()); + assertEquals("Book with name '" + book + "' can't be created", runtimeException.getMessage()); + } + + @Test + public void createAutoBook() throws Exception { + Instant now = Instant.now(); + + Duration duration = Duration.of(100, ChronoUnit.SECONDS); + long threshold = duration.dividedBy(2).toMillis(); + + BookInfo existedBook = createBookInfoWithPage(now.minus(duration), "test-existed-book"); + BookInfo newBook = createBookInfo("test-new-book", now.minus(1, ChronoUnit.DAYS)); + + CradleStorage mockStorage = mock(CradleStorage.class); + List listEntries = List.of(new BookListEntry(existedBook.getId().getName(), SCHEMA_VERSION)); + when(mockStorage.listBooks()).thenReturn(listEntries); + when(mockStorage.refreshBook(same(existedBook.getId().getName()))).thenReturn(existedBook); + when(mockStorage.addBook(argThat((bookToAdd) -> newBook.getId().getName().equals(bookToAdd.getName()) + && Instant.now().minus(1, ChronoUnit.DAYS).isAfter(bookToAdd.getCreated()) + ))).thenReturn(newBook); + when(mockStorage.addPage(same(existedBook.getId()), any(), any(), any())).thenReturn(existedBook); + when(mockStorage.addPage(same(newBook.getId()), any(), any(), any())).thenReturn(newBook); + + AutoPageConfiguration autoPageConfig = new AutoPageConfiguration(); + autoPageConfig.setPageDuration(duration); + autoPageConfig.setPageStartTime(now.plus(duration)); + + Map mapping = Map.of( + existedBook.getId().getName(), autoPageConfig, + newBook.getId().getName(), autoPageConfig + ); + try (PageManager ignored = new PageManager(mockStorage, true, mapping, 60*60*12, threshold)) { + verify(mockStorage, times(2)).listBooks(); + verify(mockStorage).refreshBook(existedBook.getId().getName()); + + verify(mockStorage).addBook(argThat((bookToAdd) -> newBook.getId().getName().equals(bookToAdd.getName()))); + + verify(mockStorage) + .addPage(same(newBook.getId()), anyString(), eq(now.minus(1, ChronoUnit.DAYS)), anyString()); + + verify(mockStorage, timeout(1000)) + .addPage(eq(existedBook.getId()), any(), eq(now.plus(duration)), any()); + verify(mockStorage, timeout(1000)) + .addPage(eq(newBook.getId()), any(), argThat((pageStart) -> + Instant.now().plus(duration).isAfter(pageStart) && Instant.now().isBefore(pageStart) + ), any()); + + verifyNoMoreInteractions(mockStorage); + } + } + + private BookInfo createBookInfoWithPage(Instant lastStarted, String fullName) { PageInfo pageInfoMock = mock(PageInfo.class); when(pageInfoMock.getStarted()).thenReturn(lastStarted); + BookInfo bookInfoMock = createBookInfo(fullName, lastStarted); + when(bookInfoMock.getLastPage()).thenReturn(pageInfoMock); + return bookInfoMock; + } + + @NotNull + private static BookInfo createBookInfo(String fullName, Instant created) { BookInfo bookInfoMock = mock(BookInfo.class); when(bookInfoMock.getId()).thenReturn(new BookId(fullName)); - when(bookInfoMock.getLastPage()).thenReturn(pageInfoMock); + when(bookInfoMock.getCreated()).thenReturn(created); when(bookInfoMock.getFullName()).thenReturn(fullName); return bookInfoMock; } diff --git a/gradle.properties b/gradle.properties index 6e46a89..0863871 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.7.2 +release_version = 1.8.0