diff --git a/LICENSE.EDL b/LICENSE.EDL new file mode 100644 index 0000000..8a8992b --- /dev/null +++ b/LICENSE.EDL @@ -0,0 +1,28 @@ +Eclipse Distribution License - v 1.0 + +Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/pom.xml b/src/pom.xml index 94ce337..93d7359 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -8,7 +8,7 @@ store-parent 2.4.0-SNAPSHOT pom - Lyo Store Parent + Lyo Store :: Parent UTF-8 @@ -19,7 +19,7 @@ store-core - store-update + store-sync @@ -55,6 +55,12 @@ xml-apis 1.3.04 + + javax.servlet + javax.servlet-api + 3.1.0 + provided + @@ -103,5 +109,9 @@ Eclipse Public License 1.0 https://www.eclipse.org/legal/epl-v10.html + + Eclipse Distribution License 1.0 + https://www.eclipse.org/org/documents/edl-v10.html + diff --git a/src/store-core/.gitignore b/src/store-core/.gitignore index afa7537..464fab7 100644 --- a/src/store-core/.gitignore +++ b/src/store-core/.gitignore @@ -13,9 +13,9 @@ hs_err_pid* # Eclipse -.project -.classpath -.metadata +/.project +/.classpath +/.metadata *.tmp *.bak *.swp @@ -63,8 +63,8 @@ local.properties # MAVEN ######################### -**/overlays/ -**/target/ +/overlays/ +/target/ pom.xml.tag pom.xml.next release.properties diff --git a/src/store-core/pom.xml b/src/store-core/pom.xml index 7baeef7..8d8045d 100644 --- a/src/store-core/pom.xml +++ b/src/store-core/pom.xml @@ -12,7 +12,7 @@ store-core - 2.4.0-SNAPSHOT + Lyo Store :: Core UTF-8 @@ -243,5 +243,9 @@ Eclipse Public License 1.0 https://www.eclipse.org/legal/epl-v10.html + + Eclipse Distribution License 1.0 + https://www.eclipse.org/org/documents/edl-v10.html + diff --git a/src/store-update/.gitignore b/src/store-sync/.gitignore similarity index 100% rename from src/store-update/.gitignore rename to src/store-sync/.gitignore diff --git a/src/store-update/LICENSE.txt b/src/store-sync/LICENSE.txt similarity index 100% rename from src/store-update/LICENSE.txt rename to src/store-sync/LICENSE.txt diff --git a/src/store-update/README.md b/src/store-sync/README.md similarity index 100% rename from src/store-update/README.md rename to src/store-sync/README.md diff --git a/src/store-update/doc/architecture.png b/src/store-sync/doc/architecture.png similarity index 100% rename from src/store-update/doc/architecture.png rename to src/store-sync/doc/architecture.png diff --git a/src/store-update/pom.xml b/src/store-sync/pom.xml similarity index 91% rename from src/store-update/pom.xml rename to src/store-sync/pom.xml index da76408..a8fd0e8 100644 --- a/src/store-update/pom.xml +++ b/src/store-sync/pom.xml @@ -7,24 +7,30 @@ org.eclipse.lyo.store store-parent - 2.3.0-SNAPSHOT + 2.4.0-SNAPSHOT ../pom.xml - store-update + store-sync + Lyo Store :: Sync UTF-8 UTF-8 1.8 1.8 - 2.3.0-SNAPSHOT + 2.4.0-SNAPSHOT + + javax.servlet + javax.servlet-api + provided + org.eclipse.lyo.store store-core - ${oslc4j-core.version} + ${version.lyo} junit @@ -39,7 +45,7 @@ org.eclipse.lyo.trs trs-provider - 2.4.0-SNAPSHOT + ${version.lyo} org.eclipse.paho @@ -86,7 +92,7 @@ 2016 - ${basedir}/src/license//eplv1/description.txt.ftl + ${basedir}/src/license/eplv1/description.txt.ftl true @@ -197,20 +203,22 @@ Eclipse Public License 1.0 https://www.eclipse.org/legal/epl-v10.html + + Eclipse Distribution License 1.0 + https://www.eclipse.org/org/documents/edl-v10.html + lyo-releases lyo-releases repository - https://repo.eclipse.org/content/repositories/lyo-releases/ - + https://repo.eclipse.org/content/repositories/lyo-releases/ lyo-snapshots lyo-snapshots repository - https://repo.eclipse.org/content/repositories/lyo-snapshots/ - + https://repo.eclipse.org/content/repositories/lyo-snapshots/ diff --git a/src/store-update/src/license/eplv1/description.txt.ftl b/src/store-sync/src/license/eplv1/description.txt.ftl similarity index 100% rename from src/store-update/src/license/eplv1/description.txt.ftl rename to src/store-sync/src/license/eplv1/description.txt.ftl diff --git a/src/store-update/src/license/eplv1/header.txt.ftl b/src/store-sync/src/license/eplv1/header.txt.ftl similarity index 100% rename from src/store-update/src/license/eplv1/header.txt.ftl rename to src/store-sync/src/license/eplv1/header.txt.ftl diff --git a/src/store-update/src/license/eplv1/license.txt b/src/store-sync/src/license/eplv1/license.txt similarity index 100% rename from src/store-update/src/license/eplv1/license.txt rename to src/store-sync/src/license/eplv1/license.txt diff --git a/src/store-update/src/license/licenses.properties b/src/store-sync/src/license/licenses.properties similarity index 100% rename from src/store-update/src/license/licenses.properties rename to src/store-sync/src/license/licenses.properties diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/Handler.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/Handler.java new file mode 100644 index 0000000..7dda34f --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/Handler.java @@ -0,0 +1,16 @@ +package org.eclipse.lyo.store.sync; + +import java.util.Collection; +import org.eclipse.lyo.store.Store; +import org.eclipse.lyo.store.sync.change.Change; + +/** + * Handler is . + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +public interface Handler { + Collection> handle(Store store, Collection> changes); +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/ServiceProviderMessage.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/ServiceProviderMessage.java new file mode 100644 index 0000000..80aaa4b --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/ServiceProviderMessage.java @@ -0,0 +1,14 @@ +package org.eclipse.lyo.store.sync; + +import java.net.URI; + +/** + * Created on 06.03.17 + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.1 + */ +public interface ServiceProviderMessage { + URI getServiceProviderUri(); +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/StoreUpdateManager.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/StoreUpdateManager.java new file mode 100644 index 0000000..a8579e6 --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/StoreUpdateManager.java @@ -0,0 +1,102 @@ +package org.eclipse.lyo.store.sync; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.eclipse.lyo.store.Store; +import org.eclipse.lyo.store.sync.change.Change; +import org.eclipse.lyo.store.sync.change.ChangeProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Schedules {@link StoreUpdateRunnable} via internal + * {@link ScheduledExecutorService} with a predefined delay or on-demand. + * Operates on a generic message that is passed to handlers. + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +public class StoreUpdateManager { + private final static Logger LOGGER = LoggerFactory.getLogger(StoreUpdateManager.class); + private static final int NODELAY = 0; + private static final int SINGLE_THREAD_POOL = 1; + private final ScheduledExecutorService executor; + private final ChangeProvider changeProvider; + private final Store store; + private final List> handlers = new ArrayList<>(); + + /** + * Schedules {@link StoreUpdateRunnable} via internal + * {@link ScheduledExecutorService} with a predefined delay or on-demand. + * + * @param store + * Instance of an initialised Store + * @param changeProvider + * Provider of the changes in the underlying tool. + */ + public StoreUpdateManager(final Store store, final ChangeProvider changeProvider) { + executor = Executors.newScheduledThreadPool(StoreUpdateManager.SINGLE_THREAD_POOL); + this.store = store; + this.changeProvider = changeProvider; + } + + /** + * Polling update on a given {@link ChangeProvider} followed by a notification + * to all previously registered {@link Handler} objects. + * + * @param lastUpdate + * Time of last store update, changes before this moment might be + * dropped. + * @param delaySeconds + * Seconds between polling checks. + */ + public void poll(final ZonedDateTime lastUpdate, final int delaySeconds) { + final StoreUpdateRunnable updateRunnable = buildRunnable(store, changeProvider, lastUpdate, null, handlers); + executor.scheduleWithFixedDelay(updateRunnable, StoreUpdateManager.NODELAY, delaySeconds, TimeUnit.SECONDS); + + StoreUpdateManager.LOGGER.trace("Poll request has been enqueued"); + } + + /** + * Submit a single update request. Typically done from the HTTP handler. + * + * @param lastUpdate + * @param message + * Specific details for the {@link ChangeProvider} + * @return {@link Future} that allows to block until the runnable is finished + * executing (strongly discouraged). + */ + public Future submit(final ZonedDateTime lastUpdate, final M message) { + final StoreUpdateRunnable updateRunnable = buildRunnable(store, changeProvider, lastUpdate, message, + handlers); + return executor.submit(updateRunnable); + } + + /** + * Add a {@link Handler} that will process the collection of {@link Change} + * generated by the {@link ChangeProvider}. Handler is not guaranteed to be + * called if added after the update has been scheduled. + * + * @param handler + * Handler that should be called whenever {@link ChangeProvider} + * returns any changes. + */ + public void addHandler(final Handler handler) { + handlers.add(handler); + } + + /** + * Method can be overridden in case a more sophisticated Runnable has to be + * constructed. + */ + protected StoreUpdateRunnable buildRunnable(final Store store, final ChangeProvider changeProvider, + final ZonedDateTime lastUpdate, final M message, final List> handlers) { + return new StoreUpdateRunnable<>(store, changeProvider, lastUpdate, message, handlers); + } +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/StoreUpdateRunnable.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/StoreUpdateRunnable.java new file mode 100644 index 0000000..88aa112 --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/StoreUpdateRunnable.java @@ -0,0 +1,94 @@ +package org.eclipse.lyo.store.sync; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import org.eclipse.lyo.store.Store; +import org.eclipse.lyo.store.sync.change.Change; +import org.eclipse.lyo.store.sync.change.ChangeProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Requests latest {@link Change} collection from {@link ChangeProvider} and triggers each + * handler afterwards. Stores the timestamp of the newest change to request new updates starting + * from that point in time. + *

+ *

Handlers will be notified with different message per service provider as accessible via + * {@link OSLCMessage#getServiceProviderId()}.

+ * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.18.0 + */ +class StoreUpdateRunnable implements Runnable { + + private static final ZonedDateTime LAST_UPDATE_DEFAULT = ZonedDateTime.of(1971, 1, 1, 1, 1, 1, + 1, ZoneId.of("UTC")); + private final Logger log = LoggerFactory.getLogger(StoreUpdateRunnable.class); + private final ChangeProvider changeProvider; + private final M message; + private final Store store; + private final List> handlers; + private ZonedDateTime lastUpdate; + + /** + * @param store Initialised Store + * @param changeProvider Change provider + * @param lastUpdate Initial timestamp. If null, will be set to {@link + * StoreUpdateRunnable#LAST_UPDATE_DEFAULT} + * @param message Initial message to the {@link ChangeProvider} + * @param handlers List of handlers to be notified. + */ + StoreUpdateRunnable(final Store store, final ChangeProvider changeProvider, + final ZonedDateTime lastUpdate, final M message, final List> handlers) { + this.store = store; + this.changeProvider = changeProvider; + this.message = message; + this.handlers = handlers; + if (lastUpdate != null) { + this.lastUpdate = lastUpdate; + } else { + this.lastUpdate = StoreUpdateRunnable.LAST_UPDATE_DEFAULT; + } + } + + @Override + public void run() { + try { + log.trace("Running background update"); + Collection> changes = null; + try { + changes = changeProvider.getChangesSince(lastUpdate, message); + } catch (final Exception e) { + log.error("ChangeProvider threw an exception", e); + } + if (changes != null && !changes.isEmpty()) { + for (final Handler handler : handlers) { + log.trace("Notifying {}", handler); + try { + handler.handle(store, changes); + } catch (final Exception e) { + log.warn("Handler {} threw an exception", handler, e); + } + } + for (final Change change : changes) { + if (change != null) { + final Date date = change.getHistoryResource().getTimestamp(); + final ZonedDateTime dateTime = date.toInstant() + .atZone(ZoneId.systemDefault()); + if (lastUpdate.isBefore(dateTime)) { + lastUpdate = dateTime; + } + } + } + log.trace("Setting previous revision to {}", lastUpdate); + } + } catch (final Exception e) { + // ExecutorService will terminate the whole schedule if a Runnable throws an exception + log.error("A handler threw an exception!", e); + } + } +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/UpdateMessage.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/UpdateMessage.java new file mode 100644 index 0000000..0bb4cef --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/UpdateMessage.java @@ -0,0 +1,5 @@ +package org.eclipse.lyo.store.sync; + +public interface UpdateMessage { + +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/Change.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/Change.java new file mode 100644 index 0000000..d891df9 --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/Change.java @@ -0,0 +1,37 @@ +package org.eclipse.lyo.store.sync.change; + +import org.eclipse.lyo.oslc4j.core.model.AbstractResource; + +/** + * Change is . + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +public class Change { + /** + * Not IExtendedResource due to the use of AbstractResource throughout Lyo generated code. + */ + private final AbstractResource resource; + private final HistoryResource historyResource; + private final T message; + + public Change(AbstractResource resource, HistoryResource historyResource, T message) { + this.resource = resource; + this.historyResource = historyResource; + this.message = message; + } + + public AbstractResource getResource() { + return resource; + } + + public HistoryResource getHistoryResource() { + return historyResource; + } + + public T getMessage() { + return message; + } +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeHelper.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeHelper.java new file mode 100644 index 0000000..ff3753c --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeHelper.java @@ -0,0 +1,37 @@ +package org.eclipse.lyo.store.sync.change; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * ChangeHelper is . + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +public class ChangeHelper { + + public static List changesCreated(Collection allChanges) { + return allChanges.stream() + .filter(change -> Objects.equals(change.getHistoryResource().getChangeKindEnum(), ChangeKind.CREATION)) + .collect(Collectors.toList()); + } + + public static List changesModified(Collection allChanges) { + return allChanges.stream().filter( + change -> Objects.equals(change.getHistoryResource().getChangeKindEnum(), ChangeKind.MODIFICATION)) + .collect(Collectors.toList()); + } + + public static List historyFrom(Collection changes) { + return ChangeHelper.mapFn(changes, Change::getHistoryResource); + } + + public static List mapFn(Collection changes, Function mapper) { + return changes.stream().map(mapper).collect(Collectors.toList()); + } +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeKind.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeKind.java new file mode 100644 index 0000000..d063b1b --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeKind.java @@ -0,0 +1,45 @@ +package org.eclipse.lyo.store.sync.change; + +import org.eclipse.lyo.oslc4j.core.annotation.OslcName; +import org.eclipse.lyo.oslc4j.core.annotation.OslcNamespace; +import org.eclipse.lyo.oslc4j.core.annotation.OslcResourceShape; + +/** + * ChangeKind is . + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +@OslcNamespace(HistoryResource.NS_TRS) +@OslcName(ChangeKind.NAME) +@OslcResourceShape(title = "Change Kind Resource Shape", describes = HistoryResource.NS_TRS + ChangeKind.NAME) +public enum ChangeKind { + // Strings taken from the TRS provider implementation + CREATION("Created"), + MODIFICATION("Modified"), + DELETION("Deleted"); + + public static final String NAME = "ChangeKind"; + private final String created; + + ChangeKind(String created) { + this.created = created; + } + + public static ChangeKind fromString(String text) { + if (text != null) { + for (ChangeKind kind : ChangeKind.values()) { + if (text.equalsIgnoreCase(kind.created)) { + return kind; + } + } + } + throw new IllegalArgumentException("No constant with text " + text + " found"); + } + + @Override + public String toString() { + return created; + } +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeProvider.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeProvider.java new file mode 100644 index 0000000..9f38ced --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/ChangeProvider.java @@ -0,0 +1,15 @@ +package org.eclipse.lyo.store.sync.change; + +import java.time.ZonedDateTime; +import java.util.Collection; + +/** + * ChangeProvider is . + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +public interface ChangeProvider { + Collection> getChangesSince(ZonedDateTime lastUpdate, T message); +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/HistoryResource.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/HistoryResource.java new file mode 100644 index 0000000..c95fc1b --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/HistoryResource.java @@ -0,0 +1,87 @@ +package org.eclipse.lyo.store.sync.change; + +import org.eclipse.lyo.oslc4j.core.annotation.OslcName; +import org.eclipse.lyo.oslc4j.core.annotation.OslcNamespace; +import org.eclipse.lyo.oslc4j.core.annotation.OslcPropertyDefinition; +import org.eclipse.lyo.oslc4j.core.annotation.OslcResourceShape; +import org.eclipse.lyo.oslc4j.core.model.AbstractResource; + +import java.net.URI; +import java.time.ZonedDateTime; +import java.util.Date; + +/** + * HistoryResource is a wrapper OSLC Resource around org.eclipse.lyo.oslc4j.trs.provider.HistoryData. + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +@OslcNamespace(HistoryResource.NS_TRS) +@OslcName(HistoryResource.NAME) +@OslcResourceShape(title = "TRS History Resource Shape", describes = HistoryResource.TYPE) +public class HistoryResource extends AbstractResource { + public static final String NS_TRS = "http://open-services.net/ns/core/trs#"; + public static final String NAME = "TrsHistoryResource"; + public static final String TYPE = NS_TRS + NAME; + private ChangeKind changeKind; + private Date timestamp; + private URI resourceURI; + + /** + * Shall be used only by the OSLC Jena Model Helper + */ + @Deprecated + public HistoryResource() { + } + + public HistoryResource(ChangeKind changeKind, Date timestamp, URI resourceURI) { + this.changeKind = changeKind; + this.timestamp = timestamp; + this.resourceURI = resourceURI; + } + + public HistoryResource(ChangeKind changeKind, ZonedDateTime timestamp, URI resourceURI) { + this.changeKind = changeKind; + this.timestamp = Date.from(timestamp.toInstant()); + this.resourceURI = resourceURI; + } + + @OslcName("change_kind") + @OslcPropertyDefinition(NS_TRS + "change_kind") + public String getChangeKind() { + return changeKind.toString(); + } + + public void setChangeKind(ChangeKind changeKind) { + this.changeKind = changeKind; + } + + public ChangeKind getChangeKindEnum() { + return changeKind; + } + + public void setChangeKind(String changeKind) { + this.changeKind = ChangeKind.fromString(changeKind); + } + + @OslcName("timestamp") + @OslcPropertyDefinition(NS_TRS + "timestamp") + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + @OslcName("uri") + @OslcPropertyDefinition(NS_TRS + "uri") + public URI getResourceURI() { + return resourceURI; + } + + public void setResourceURI(URI resourceURI) { + this.resourceURI = resourceURI; + } +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/package-info.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/package-info.java new file mode 100644 index 0000000..550f3de --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/change/package-info.java @@ -0,0 +1,9 @@ +/** + * Classes for defining changes for handlers. + * {@link org.eclipse.lyo.store.sync.change.HistoryResource} can be persisted in a + * triplestore. + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.18.0 + */ +package org.eclipse.lyo.store.sync.change; diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/SimpleStoreHandler.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/SimpleStoreHandler.java new file mode 100644 index 0000000..7bf12b1 --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/SimpleStoreHandler.java @@ -0,0 +1,103 @@ +package org.eclipse.lyo.store.sync.handlers; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.lyo.oslc4j.core.model.AbstractResource; +import org.eclipse.lyo.oslc4j.core.model.ServiceProvider; +import org.eclipse.lyo.store.Store; +import org.eclipse.lyo.store.StoreAccessException; +import org.eclipse.lyo.store.sync.Handler; +import org.eclipse.lyo.store.sync.ServiceProviderMessage; +import org.eclipse.lyo.store.sync.change.Change; +import org.eclipse.lyo.store.sync.change.ChangeKind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a simple handler that puts resources into named graphs per + * {@link ServiceProvider} if the message implements + * {@link ServiceProviderMessage}. + * + * @author Andrew Berezovskyi + * @version $version-stub$ + * @param + * @since 2.4.0 + * + */ +public class SimpleStoreHandler implements Handler { + + private final URI defaultGraph; + + private final Logger log = LoggerFactory.getLogger(SimpleStoreHandler.class); + + private final Store store; + + public SimpleStoreHandler(Store store) { + this.store = store; + try { + this.defaultGraph = new URI("urn:x-arq:DefaultGraph"); + } catch (URISyntaxException e) { + // this should never happen - we don't want the exception trail + // so we cheat with wrapping this in an unmanaged exception + // that will halt the execution. + IllegalStateException exception = new IllegalStateException(e); + log.error("Failed to generate default graph URI"); + throw exception; + } + } + + @Override + public Collection> handle(Store store, Collection> changes) { + if (changes == null || changes.isEmpty()) { + log.warn("Empty change list cannot be handled"); + return changes; + } + + Optional> firstChange = changes.stream().findFirst(); + if (firstChange.get().getMessage() instanceof ServiceProviderMessage) { + Map>> map = changes.stream() + .collect(Collectors.groupingBy(c -> c.getMessage().getClass())); + for (Entry>> changeSet : map.entrySet()) { + URI spURI = ((ServiceProviderMessage) changeSet.getKey()).getServiceProviderUri(); + persistChanges(spURI, changeSet.getValue()); + } + } else { + persistChanges(defaultGraph, changes); + } + + return changes; + } + + private void persistChanges(URI spURI, Collection> value) { + try { + persistUpdates(spURI, value); + } catch (StoreAccessException e) { + log.error("Failed to persist updates", e); + } + + persistDeletions(spURI, value); + } + + private void persistDeletions(URI spURI, Collection> value) { + Collection deletedResources = value.stream() + .filter(c -> c.getHistoryResource().getChangeKindEnum() == ChangeKind.DELETION) + .map(c -> c.getResource().getAbout()).collect(Collectors.toList()); + store.deleteResources(spURI, deletedResources.toArray(new URI[0])); + } + + private void persistUpdates(URI spURI, Collection> value) throws StoreAccessException { + Collection updatedResources = value.stream() + .filter(c -> c.getHistoryResource().getChangeKindEnum() == ChangeKind.CREATION + || c.getHistoryResource().getChangeKindEnum() == ChangeKind.MODIFICATION) + .map(c -> c.getResource()).collect(Collectors.toList()); + store.appendResources(spURI, updatedResources); + } + +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/TrsHandler.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/TrsHandler.java new file mode 100644 index 0000000..f71eea8 --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/TrsHandler.java @@ -0,0 +1,58 @@ +package org.eclipse.lyo.store.sync.handlers; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.lyo.oslc4j.trs.provider.ChangeHistories; +import org.eclipse.lyo.oslc4j.trs.provider.HistoryData; +import org.eclipse.lyo.store.Store; +import org.eclipse.lyo.store.sync.Handler; +import org.eclipse.lyo.store.sync.change.Change; +import org.eclipse.lyo.store.sync.change.ChangeKind; +import org.eclipse.lyo.store.sync.change.HistoryResource; + +public class TrsHandler implements Handler { + + private ChangeHistories changeLog; + + public TrsHandler(ChangeHistories changeLog) { + this.changeLog = changeLog; + } + + @Override + public Collection> handle(Store store, Collection> changes) { + updateChangelogHistory(changes); + return changes; + } + + protected List updateChangelogHistory(Collection> changes) { + List changesHistory = changesHistory(changes); + changeLog.updateHistories(changesHistory); + return changesHistory; + } + + private List changesHistory(Collection> changes) { + return changes.stream().map(this::historyElementFromChange).collect(Collectors.toList()); + } + + private HistoryData historyElementFromChange(Change change) { + HistoryResource h = change.getHistoryResource(); + HistoryData historyData = HistoryData.getInstance(h.getTimestamp(), h.getResourceURI(), + historyDataType(h.getChangeKindEnum())); + return historyData; + } + + private String historyDataType(ChangeKind changeKind) { + if (changeKind.equals(ChangeKind.CREATION)) { + return HistoryData.CREATED; + } else if (changeKind.equals(ChangeKind.MODIFICATION)) { + return HistoryData.MODIFIED; + } else if (changeKind.equals(ChangeKind.DELETION)) { + return HistoryData.DELETED; + } else { + throw new IllegalArgumentException("Illegal ChangeKind value"); + } + } + +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/TrsMqttChangeLogHandler.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/TrsMqttChangeLogHandler.java new file mode 100644 index 0000000..307910a --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/TrsMqttChangeLogHandler.java @@ -0,0 +1,100 @@ +package org.eclipse.lyo.store.sync.handlers; + +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.List; +import java.util.TimeZone; + +import javax.xml.datatype.DatatypeConfigurationException; + +import org.eclipse.lyo.core.trs.ChangeEvent; +import org.eclipse.lyo.core.trs.Creation; +import org.eclipse.lyo.core.trs.Deletion; +import org.eclipse.lyo.core.trs.Modification; +import org.eclipse.lyo.oslc4j.core.exception.OslcCoreApplicationException; +import org.eclipse.lyo.oslc4j.core.model.AbstractResource; +import org.eclipse.lyo.oslc4j.provider.jena.JenaModelHelper; +import org.eclipse.lyo.oslc4j.trs.provider.ChangeHistories; +import org.eclipse.lyo.oslc4j.trs.provider.HistoryData; +import org.eclipse.lyo.store.sync.change.Change; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.jena.rdf.model.Model; + +public class TrsMqttChangeLogHandler extends TrsHandler { + private final Logger log = LoggerFactory.getLogger(TrsMqttChangeLogHandler.class); + + private final MqttClient mqttClient; + private final String topic; + private int order; + + public TrsMqttChangeLogHandler(final ChangeHistories changeLog, final MqttClient mqttClient, final String topic) { + super(changeLog); + this.mqttClient = mqttClient; + this.topic = topic; + // FIXME Andrew@2018-03-11: may and should blow up, but bringing back + this.order = changeLog.getHistory(null, null).length; + } + + @Override + protected List updateChangelogHistory(Collection> changes) { + List updateChangelogHistory = super.updateChangelogHistory(changes); + for (HistoryData historyData : updateChangelogHistory) { + AbstractResource res = trsChangeResourceFrom(historyData); + MqttMessage message = buildMqttMessage(res); + try { + mqttClient.publish(topic, message); + } catch (MqttException e) { + log.error("Can't publish the message to the MQTT channel", e); + } + } + return updateChangelogHistory; + } + + private MqttMessage buildMqttMessage(AbstractResource res) { + try { + Model changeEventJenaModel = JenaModelHelper.createJenaModel(new Object[] { res }); + MqttMessage message = new MqttMessage(); + message.setPayload(changeEventJenaModel.toString().getBytes()); + return message; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException + | DatatypeConfigurationException | OslcCoreApplicationException e) { + throw new IllegalArgumentException(e); + } + } + + private AbstractResource trsChangeResourceFrom(HistoryData historyData) { + // FIXME Andrew@2018-03-11: not thread-safe + this.order += 1; + String histDataType = historyData.getType(); + URI uri = historyData.getUri(); + URI changedUri; + try { + TimeZone tz = TimeZone.getTimeZone("UTC"); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); // Quoted "Z" to indicate UTC, no timezone offset + df.setTimeZone(tz); + String nowAsISO = df.format(historyData.getTimestamp()); + changedUri = new URI("urn:x-trs:" + nowAsISO + ":" + this.order); + ChangeEvent ce; + if (histDataType == HistoryData.CREATED) { + ce = new Creation(changedUri, uri, this.order); + } else if (histDataType == HistoryData.MODIFIED) { + ce = new Modification(changedUri, uri, this.order); + } else { + ce = new Deletion(changedUri, uri, this.order); + } + return ce; + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/package-info.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/package-info.java new file mode 100644 index 0000000..c27aae5 --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/handlers/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author andrew + * + */ +package org.eclipse.lyo.store.sync.handlers; diff --git a/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/package-info.java b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/package-info.java new file mode 100644 index 0000000..74482ab --- /dev/null +++ b/src/store-sync/src/main/java/org/eclipse/lyo/store/sync/package-info.java @@ -0,0 +1,10 @@ +/** + * Common primitives for updating store. + * {@link org.eclipse.lyo.store.sync.StoreUpdateRunnable} is scheduled by + * {@link org.eclipse.lyo.store.sync.StoreUpdateManager}. + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.18.0 + */ +package org.eclipse.lyo.store.sync; diff --git a/src/store-sync/src/test/java/org/eclipse/lyo/store/sync/TestHistoryResource.java b/src/store-sync/src/test/java/org/eclipse/lyo/store/sync/TestHistoryResource.java new file mode 100644 index 0000000..882db4c --- /dev/null +++ b/src/store-sync/src/test/java/org/eclipse/lyo/store/sync/TestHistoryResource.java @@ -0,0 +1,86 @@ +package org.eclipse.lyo.store.sync; + +import org.apache.jena.rdf.model.Model; +import java.net.URI; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import org.eclipse.lyo.oslc4j.provider.jena.JenaModelHelper; +import org.eclipse.lyo.store.Store; +import org.eclipse.lyo.store.StoreFactory; +import org.eclipse.lyo.store.sync.change.ChangeKind; +import org.eclipse.lyo.store.sync.change.HistoryResource; +import org.junit.Test; + +/** + * TestTrsHistoryResource is . + * + * @author Andrew Berezovskyi (andriib@kth.se) + * @version $version-stub$ + * @since 0.0.0 + */ +public class TestHistoryResource { + public static final URI RESOURCE_URI = URI.create("test:test"); + private final Store store = StoreFactory.inMemory(); + + @Test + public void testResourceIsMarshalled() throws Exception { + HistoryResource resource = new HistoryResource(ChangeKind.CREATION, + Date.from(Instant.now()), TestHistoryResource.RESOURCE_URI); + + Model model = JenaModelHelper.createJenaModel(new Object[] { resource }); + assertThat(model.size()).isGreaterThan(0); + } + + @Test + public void testResourceContainsStatements() throws Exception { + HistoryResource resource = new HistoryResource(ChangeKind.CREATION, + Date.from(Instant.now()), TestHistoryResource.RESOURCE_URI); + resource.setAbout(TestHistoryResource.RESOURCE_URI); + + Model model = JenaModelHelper.createJenaModel(new Object[] { resource }); + assertThat(model.size()).isGreaterThan(1); + } + + @Test + public void testResourceIsPersisted() throws Exception { + HistoryResource resource = new HistoryResource(ChangeKind.CREATION, + Date.from(Instant.now()), TestHistoryResource.RESOURCE_URI); + resource.setAbout(TestHistoryResource.RESOURCE_URI); + + assertThat(store.keySet()).hasSize(0); + store.putResources(resource.getAbout(), Collections.singletonList(resource)); + assertThat(store.keySet()).hasSize(1); + } + + @Test + public void testResourceIsRestored() throws Exception { + HistoryResource resource = new HistoryResource(ChangeKind.CREATION, + Date.from(Instant.now()), TestHistoryResource.RESOURCE_URI); + resource.setAbout(TestHistoryResource.RESOURCE_URI); + + store.putResources(resource.getAbout(), Collections.singletonList(resource)); + List resources = store.getResources(TestHistoryResource.RESOURCE_URI, HistoryResource.class); + assertThat(resources).hasSize(1); + } + + @Test + public void testResourceIsRestoredWithProperties() throws Exception { + String testResourceURI = "lyo:testtest"; + Date timestamp = Date.from(Instant.now()); + HistoryResource resource = new HistoryResource(ChangeKind.CREATION, timestamp, + URI.create(testResourceURI)); + resource.setAbout(TestHistoryResource.RESOURCE_URI); + + store.putResources(resource.getAbout(), Collections.singletonList(resource)); + + List resources = store.getResources(TestHistoryResource.RESOURCE_URI, HistoryResource.class); + HistoryResource storeResource = resources.get(0); + + assertThat(storeResource.getChangeKind()).isEqualToIgnoringCase(String.valueOf(ChangeKind.CREATION)); + assertThat(storeResource.getTimestamp()).isEqualTo(timestamp); + assertThat(storeResource.getResourceURI().toASCIIString()).isEqualTo(testResourceURI); + } +} diff --git a/src/store-update/src/test/resources/log4j.properties b/src/store-sync/src/test/resources/log4j.properties similarity index 100% rename from src/store-update/src/test/resources/log4j.properties rename to src/store-sync/src/test/resources/log4j.properties