| /******************************************************************************** |
| * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation |
| * |
| * See the NOTICE file(s) distributed with this work for additional |
| * information regarding copyright ownership. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| ********************************************************************************/ |
| |
| package org.eclipse.mdm.freetextindexer.boundary; |
| |
| import java.security.Principal; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.concurrent.ScheduledFuture; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| |
| import javax.annotation.PostConstruct; |
| import javax.annotation.PreDestroy; |
| import javax.annotation.Resource; |
| import javax.ejb.EJB; |
| import javax.ejb.Schedule; |
| import javax.ejb.Singleton; |
| import javax.ejb.Startup; |
| import javax.ejb.TransactionAttribute; |
| import javax.ejb.TransactionAttributeType; |
| import javax.enterprise.concurrent.ManagedScheduledExecutorService; |
| import javax.inject.Inject; |
| |
| import org.eclipse.mdm.api.base.ConnectionException; |
| import org.eclipse.mdm.api.base.ServiceNotProvidedException; |
| import org.eclipse.mdm.api.base.adapter.EntityType; |
| import org.eclipse.mdm.api.base.model.Entity; |
| import org.eclipse.mdm.api.base.model.User; |
| import org.eclipse.mdm.api.base.notification.NotificationException; |
| import org.eclipse.mdm.api.base.notification.NotificationFilter; |
| import org.eclipse.mdm.api.base.notification.NotificationListener; |
| import org.eclipse.mdm.api.base.notification.NotificationService; |
| import org.eclipse.mdm.api.base.query.DataAccessException; |
| import org.eclipse.mdm.api.dflt.ApplicationContext; |
| import org.eclipse.mdm.api.dflt.EntityManager; |
| import org.eclipse.mdm.connector.boundary.ConnectorService; |
| import org.eclipse.mdm.freetextindexer.control.DatabaseLockHandler; |
| import org.eclipse.mdm.freetextindexer.control.LockListener; |
| import org.eclipse.mdm.freetextindexer.control.UpdateIndex; |
| import org.eclipse.mdm.freetextindexer.entities.MDMEntityResponse; |
| import org.eclipse.mdm.property.GlobalProperty; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * This boundary is a back-end Boundary to the openMDM Api. It uses the Seach |
| * Server to build up MDDocuments. |
| * |
| * @author CWE |
| * |
| */ |
| @TransactionAttribute(value = TransactionAttributeType.NOT_SUPPORTED) |
| @Startup |
| @Singleton |
| public class MdmApiBoundary { |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(MdmApiBoundary.class); |
| |
| private static final String FREETEXT_NOTIFICATION_NAME = "freetext.notificationName"; |
| private static final String FREETEXT_SESSION_CHECK_INTERVAL = "freetext.sessionCheckInterval"; |
| |
| public class FreeTextNotificationListener implements NotificationListener { |
| |
| private EntityManager entityManager; |
| |
| public FreeTextNotificationListener(EntityManager entityManager) { |
| this.entityManager = entityManager; |
| } |
| |
| @Override |
| public void instanceCreated(List<? extends Entity> entities, User arg1) { |
| LOGGER.debug("{} entities created: {}", entities.size(), entities); |
| entities.forEach(e -> update.change(MDMEntityResponse.build(e.getClass(), e, entityManager))); |
| } |
| |
| @Override |
| public void instanceDeleted(EntityType entityType, List<String> ids, User user) { |
| LOGGER.debug("{} entities deleted: {}", ids.size(), ids); |
| ids.forEach(id -> update.delete(getApiName(entityManager), workaroundForTypeMapping(entityType), id)); |
| } |
| |
| @Override |
| public void instanceModified(List<? extends Entity> entities, User arg1) { |
| LOGGER.debug("{} entities modified: {}", entities.size(), entities); |
| entities.forEach(e -> update.change(MDMEntityResponse.build(e.getClass(), e, entityManager))); |
| } |
| |
| @Override |
| public void modelModified(EntityType arg0, User arg1) { |
| // not needed |
| } |
| |
| @Override |
| public void securityModified(EntityType entityType, List<String> ids, User user) { |
| // not needed |
| } |
| } |
| |
| @Inject |
| UpdateIndex update; |
| |
| @Resource |
| ManagedScheduledExecutorService scheduler; |
| |
| @Inject |
| @GlobalProperty(value = "freetext.active") |
| private String active = "false"; |
| |
| ConnectorService connectorService; |
| |
| @Inject |
| @GlobalProperty |
| private Map<String, String> globalProperties = Collections.emptyMap(); |
| |
| private ScheduledFuture<?> scheduledFuture; |
| |
| @EJB |
| DatabaseLockHandler databaseLockHandler; |
| |
| @PostConstruct |
| public void initalize() { |
| if (!isActive()) { |
| return; |
| } |
| |
| databaseLockHandler.init("free-text-indexer", new LockListener() { |
| |
| @Override |
| public void onLockAcquired() { |
| LOGGER.info("Acquired database lock, starting fretextindexer."); |
| |
| Principal principal = new Principal() { |
| |
| @Override |
| public String getName() { |
| return null; |
| } |
| }; |
| |
| connectorService = new ConnectorService(principal, globalProperties); |
| connectorService.connect(); |
| connectorService.getContexts().forEach(MdmApiBoundary.this::initializeContext); |
| |
| long intervalSeconds = 60L; |
| try { |
| Long.parseLong(globalProperties.getOrDefault(FREETEXT_SESSION_CHECK_INTERVAL, |
| "60")); |
| } catch (NumberFormatException e) { |
| LOGGER.warn("Could not parse value for parameter '" + |
| FREETEXT_SESSION_CHECK_INTERVAL + "'. Using default value '60'."); |
| } |
| |
| scheduledFuture = MdmApiBoundary.this.scheduler.scheduleAtFixedRate( |
| MdmApiBoundary.this::sessionCheck, intervalSeconds, intervalSeconds, |
| TimeUnit.SECONDS); |
| |
| LOGGER.info("Initialized session tick timer with interval {}s.", |
| intervalSeconds); |
| } |
| |
| @Override |
| public void onLockLost() { |
| deregister(); |
| } |
| }); |
| databaseLockHandler.requestLock(); |
| } |
| |
| /** |
| * Tries in intervals of one minute to acquire the database lock. If the lock |
| * could be acquired, the onLockAcquired method on the registered listener is |
| * invoked. |
| */ |
| @Schedule(hour = "*", minute = "*/1", persistent = false) |
| public void checkLock() { |
| databaseLockHandler.requestLock(); |
| } |
| |
| private void initializeContext(ApplicationContext context) { |
| |
| String source = getName(context); |
| |
| try { |
| EntityManager entityManager = context.getEntityManager() |
| .orElseThrow(() -> new ServiceNotProvidedException(EntityManager.class)); |
| |
| NotificationService manager = context.getNotificationService() |
| .orElseThrow(() -> new ConnectionException("Context has no NotificationManager!")); |
| |
| String notificationName = context.getParameters().getOrDefault(FREETEXT_NOTIFICATION_NAME, "mdm5"); |
| LOGGER.debug("Registering with name '{}' at source '{}'", notificationName, source); |
| |
| manager.register(notificationName, new NotificationFilter(), |
| new FreeTextNotificationListener(entityManager)); |
| LOGGER.info("Successfully registered for new notifications with name '{}' at source '{}!", notificationName, |
| source); |
| } catch (ConnectionException | NotificationException e) { |
| throw new IllegalArgumentException( |
| "The ODS Server and/or the Notification Service cannot be accessed for source '" + source + "'!", |
| e); |
| } |
| } |
| |
| public void sessionCheck() { |
| LOGGER.debug("Session check on {} context(s).", connectorService.getContexts().size()); |
| |
| for (ApplicationContext context : connectorService.getContexts()) { |
| try { |
| Optional<EntityManager> em = context.getEntityManager(); |
| if (em.isPresent()) { |
| em.get().loadLoggedOnUser(); |
| } |
| } catch (Exception e) { |
| LOGGER.warn("ApplicationContext seems to be closed. Trying to reconnect."); |
| |
| try { |
| Optional<ApplicationContext> c = connectorService.reconnect(context); |
| initializeContext(c.get()); |
| LOGGER.info("Freetextindexer ApplicationContext reconnected."); |
| } catch (Exception ex) { |
| LOGGER.info("Could not reconnect Freetextindexer ApplicationContext!", ex); |
| } |
| } |
| } |
| } |
| |
| @PreDestroy |
| public void deregister() { |
| if (isActive()) { |
| scheduledFuture.cancel(true); |
| |
| for (ApplicationContext context : getContexts().values()) { |
| try { |
| context.getNotificationService() |
| .orElseThrow(() -> new ConnectionException("Context has no NotificationManager!")) |
| .close(false); |
| |
| } catch (ConnectionException | NotificationException e) { |
| throw new IllegalStateException( |
| "The NotificationManager could not be deregistered. In rare cases, this leads to a missed notification. This means the index might not be up-to-date."); |
| } |
| } |
| |
| connectorService.disconnect(); |
| } |
| if (databaseLockHandler.hasDatabaseLock()) { |
| databaseLockHandler.releaseDatabaseLock(); |
| } |
| } |
| |
| public void doForAllEntities(Class<? extends Entity> entityClass, ApplicationContext context, |
| Consumer<? super MDMEntityResponse> executor) { |
| if (isActive()) { |
| try { |
| EntityManager entityManager = context.getEntityManager() |
| .orElseThrow(() -> new ServiceNotProvidedException(EntityManager.class)); |
| |
| entityManager.loadAll(entityClass).stream().map(e -> this.buildEntity(e, entityManager)) |
| .forEach(executor); |
| |
| } catch (DataAccessException e) { |
| throw new IllegalStateException("MDM cannot be querried for new elements. Please check the MDM runtime", |
| e); |
| } |
| } |
| } |
| |
| public Map<String, ApplicationContext> getContexts() { |
| if (connectorService == null) { |
| return Collections.emptyMap(); |
| } |
| return connectorService.getContexts().stream().collect(Collectors.toMap(this::getName, Function.identity())); |
| } |
| |
| public String getName(ApplicationContext context) { |
| return context.getEntityManager().map(this::getApiName) |
| .orElseThrow(() -> new ServiceNotProvidedException(EntityManager.class)); |
| } |
| |
| private String getApiName(EntityManager entityManager) { |
| return entityManager.loadEnvironment().getSourceName(); |
| } |
| |
| private boolean isActive() { |
| return Boolean.parseBoolean(active); |
| } |
| |
| private MDMEntityResponse buildEntity(Entity e, EntityManager entityManager) { |
| return MDMEntityResponse.build(e.getClass(), e, entityManager); |
| } |
| |
| /** |
| * Simple workaround for naming mismatch between Adapter and Business object |
| * names. |
| * |
| * @param entityType entity type |
| * @return MDM business object name |
| */ |
| private static String workaroundForTypeMapping(EntityType entityType) { |
| switch (entityType.getName()) { |
| case "StructureLevel": |
| return "Pool"; |
| case "MeaResult": |
| return "Measurement"; |
| case "SubMatrix": |
| return "ChannelGroup"; |
| case "MeaQuantity": |
| return "Channel"; |
| default: |
| return entityType.getName(); |
| } |
| } |
| |
| } |