/** * */ package com.thinkbiganalytics.alerts.spi.defaults; /*- * #%L * thinkbig-alerts-default * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * 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. * #L% */ import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Predicate; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.thinkbiganalytics.alerts.api.Alert; import com.thinkbiganalytics.alerts.api.Alert.ID; import com.thinkbiganalytics.alerts.api.Alert.Level; import com.thinkbiganalytics.alerts.api.Alert.State; import com.thinkbiganalytics.alerts.api.AlertChangeEvent; import com.thinkbiganalytics.alerts.api.AlertCriteria; import com.thinkbiganalytics.alerts.api.AlertNotfoundException; import com.thinkbiganalytics.alerts.api.AlertResponse; import com.thinkbiganalytics.alerts.api.core.BaseAlertCriteria; import com.thinkbiganalytics.alerts.spi.AlertDescriptor; import com.thinkbiganalytics.alerts.spi.AlertManager; import com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver; import com.thinkbiganalytics.metadata.api.MetadataAccess; import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlert; import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlert.AlertId; import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlertChangeEvent; import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlertRepository; import com.thinkbiganalytics.metadata.jpa.alerts.QJpaAlert; import org.joda.time.DateTime; import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport; import org.springframework.security.core.context.SecurityContextHolder; import java.io.Serializable; import java.net.URI; import java.security.Principal; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.inject.Inject; /** * */ public class DefaultAlertManager extends QueryDslRepositorySupport implements AlertManager { @Inject private JPAQueryFactory queryFactory; @Inject private MetadataAccess metadataAccess; private Set<AlertNotifyReceiver> alertReceivers = Collections.synchronizedSet(new HashSet<>()); private JpaAlertRepository repository; /** * @param repo */ public DefaultAlertManager(JpaAlertRepository repo) { super(JpaAlert.class); this.repository = repo; } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSource#resolve(java.io.Serializable) */ @Override public ID resolve(Serializable id) { if (id instanceof JpaAlert.AlertId) { return (JpaAlert.AlertId) id; } else { return new JpaAlert.AlertId(id); } } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlertDescriptors() */ @Override public Set<AlertDescriptor> getAlertDescriptors() { // TODO Auto-generated method stub return null; } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSource#addReceiver(com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver) */ @Override public void addReceiver(AlertNotifyReceiver receiver) { this.alertReceivers.add(receiver); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSource#removeReceiver(com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver) */ @Override public void removeReceiver(AlertNotifyReceiver receiver) { this.alertReceivers.remove(receiver); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSource#criteria() */ @Override public AlertCriteria criteria() { return new Criteria(); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlert(com.thinkbiganalytics.alerts.api.Alert.ID) */ @Override public Optional<Alert> getAlert(ID id) { return this.metadataAccess.read(() -> { return Optional.of(findAlert(id).map(a -> asValue(a)).orElseThrow(() -> new AlertNotfoundException(id))); }, MetadataAccess.SERVICE); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlerts() */ @Override public Iterator<Alert> getAlerts(AlertCriteria criteria) { return this.metadataAccess.read(() -> { Criteria critImpl = (Criteria) (criteria == null ? criteria() : criteria); return critImpl.createQuery().fetch().stream() .map(a -> asValue(a)) .collect(Collectors.toList()) // Need to terminate the stream while still in a transaction .iterator(); }, MetadataAccess.SERVICE); } // // /* (non-Javadoc) // * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlerts(org.joda.time.DateTime) // */ // @Override // public Iterator<Alert> getAlerts(DateTime since) { // return this.metadataAccess.read(() -> { // return repository.findAlertsAfter(since).stream() // .map(a -> asValue(a)) // .collect(Collectors.toList()) // Need to terminate the stream while still in a transaction // .iterator(); // }, MetadataAccess.SERVICE); // } // // /* (non-Javadoc) // * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlerts(com.thinkbiganalytics.alerts.api.Alert.ID) // */ // @Override // public Iterator<Alert> getAlerts(ID since) { // return this.metadataAccess.read(() -> { // return getAlert(since) // .map(a -> getAlerts(a.getCreatedTime())) // .orElseThrow(() -> new AlertNotfoundException(since)); // }, MetadataAccess.SERVICE); // } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertManager#addDescriptor(com.thinkbiganalytics.alerts.spi.AlertDescriptor) */ @Override public boolean addDescriptor(AlertDescriptor descriptor) { // TODO Auto-generated method stub return false; } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertManager#create(java.net.URI, com.thinkbiganalytics.alerts.api.Alert.Level, java.lang.String, java.io.Serializable) */ @Override public <C extends Serializable> Alert create(URI type, Level level, String description, C content) { final Principal user = SecurityContextHolder.getContext().getAuthentication() != null ? SecurityContextHolder.getContext().getAuthentication() : null; Alert created = this.metadataAccess.commit(() -> { JpaAlert alert = new JpaAlert(type, level, user, description, content); this.repository.save(alert); return asValue(alert); }, MetadataAccess.SERVICE); notifyReceivers(1); return created; } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertManager#getResponse(com.thinkbiganalytics.alerts.api.Alert) */ @Override public AlertResponse getResponse(Alert alert) { JpaAlert.AlertId idImpl = (JpaAlert.AlertId) resolve(alert.getId()); return new TransactionalResponse(idImpl); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertManager#remove(com.thinkbiganalytics.alerts.api.Alert.ID) */ @Override public Alert remove(ID id) { JpaAlert.AlertId idImpl = (JpaAlert.AlertId) resolve(id); return this.metadataAccess.commit(() -> { JpaAlert alert = repository.findOne(idImpl); this.repository.delete(id); return alert; }, MetadataAccess.SERVICE); } protected Optional<JpaAlert> findAlert(Alert.ID id) { JpaAlert.AlertId idImpl = (AlertId) resolve(id); return Optional.ofNullable(repository.findOne(idImpl)); } protected Alert asValue(Alert alert) { return new ImmutableAlert(alert, this); } protected JpaAlert clearAlert(JpaAlert.AlertId id) { return this.metadataAccess.commit(() -> { JpaAlert alert = repository.findOne(id); alert.setCleared(true); return alert; }, MetadataAccess.SERVICE); } protected <C extends Serializable> Alert changeAlert(JpaAlert.AlertId id, State state, String descr, C content) { final Principal user = SecurityContextHolder.getContext().getAuthentication() != null ? SecurityContextHolder.getContext().getAuthentication() : null; Alert changed = this.metadataAccess.commit(() -> { JpaAlert alert = findAlert(id).orElseThrow(() -> new AlertNotfoundException(id)); JpaAlertChangeEvent event = new JpaAlertChangeEvent(state, user, descr, content); alert.addEvent(event); return asValue(alert); }, MetadataAccess.SERVICE); notifyReceivers(1); return changed; } protected void notifyReceivers(int count) { Set<AlertNotifyReceiver> receivers; synchronized (this.alertReceivers) { receivers = new HashSet<>(this.alertReceivers); } receivers.forEach(a -> a.alertsAvailable(count)); } private static class ImmutableAlert implements Alert { private final AlertManager source; private final Alert.ID id; private final String description; private final Level level; private final URI type; private final DateTime createdTime; private final Serializable content; private final boolean cleared; private final List<AlertChangeEvent> events; public ImmutableAlert(Alert alert, AlertManager mgr) { this.source = mgr; this.id = alert.getId(); this.content = alert.getContent(); this.description = alert.getDescription(); this.level = alert.getLevel(); this.type = alert.getType(); this.cleared = alert.isCleared(); this.createdTime = alert.getCreatedTime(); this.events = Collections.unmodifiableList(alert.getEvents().stream() .map(a -> new ImmutableAlertChangeEvent(a)) .collect(Collectors.toList())); } @Override public AlertManager getSource() { return source; } @Override public Alert.ID getId() { return id; } @Override public String getDescription() { return description; } @Override public Level getLevel() { return level; } @Override public URI getType() { return type; } @Override public State getState() { return this.events.get(0).getState(); } @Override @SuppressWarnings("unchecked") public Serializable getContent() { return content; } @Override public List<AlertChangeEvent> getEvents() { return events; } @Override public DateTime getCreatedTime() { return this.createdTime; } @Override public boolean isCleared() { return this.cleared; } @Override public boolean isActionable() { return true; } } private static class ImmutableAlertChangeEvent implements AlertChangeEvent { private final DateTime changeTime; private final State state; private final Principal user; private final String description; private final Serializable content; public ImmutableAlertChangeEvent(AlertChangeEvent event) { this.changeTime = event.getChangeTime(); this.state = event.getState(); this.user = event.getUser(); this.description = event.getDescription(); this.content = event.getContent(); } public DateTime getChangeTime() { return changeTime; } public State getState() { return state; } @Override public Principal getUser() { return user; } @Override public String getDescription() { return this.description; } public Serializable getContent() { return this.content; } } private class TransactionalResponse implements AlertResponse { private final JpaAlert.AlertId id; public TransactionalResponse(JpaAlert.AlertId id) { super(); this.id = id; } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertResponse#inProgress() */ @Override public Alert inProgress(String descr) { return inProgress(descr, null); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertResponse#inProgress(java.io.Serializable) */ @Override public <C extends Serializable> Alert inProgress(String descr, C content) { return changeAlert(this.id, State.IN_PROGRESS, descr, content); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertResponse#handle() */ @Override public Alert handle(String descr) { return handle(descr, null); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertResponse#handle(java.io.Serializable) */ @Override public <C extends Serializable> Alert handle(String descr, C content) { return changeAlert(this.id, State.HANDLED, descr, content); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertResponse#unHandle() */ @Override public Alert unhandle(String descr) { return unhandle(descr, null); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertResponse#unhandle(java.io.Serializable) */ @Override public <C extends Serializable> Alert unhandle(String descr, C content) { return changeAlert(this.id, State.UNHANDLED, descr, content); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertResponse#clear() */ @Override public void clear() { clearAlert(this.id); } } private class Criteria extends BaseAlertCriteria { public JPAQuery<JpaAlert> createQuery() { List<Predicate> preds = new ArrayList<>(); QJpaAlert alert = QJpaAlert.jpaAlert; JPAQuery<JpaAlert> query = queryFactory .select(alert) .from(alert) .limit(getLimit()); // The "state" criteria now means filter by an alert's current state. // To support filtering by any state the alert has transitioned through // we can add the commented out code below (the old state behavior) // if (getTransitions().size() > 0) { // QAlertChangeEvent event = QAlertChangeEvent.alertChangeEvent; // query.join(alert.events, event); // preds.add(event.state.in(getTransitions())); // } if (getStates().size() > 0) { preds.add(alert.state.in(getStates())); } if (getLevels().size() > 0) { preds.add(alert.level.in(getLevels())); } if (getAfterTime() != null) { preds.add(alert.createdTime.gt(getAfterTime())); } if (getBeforeTime() != null) { preds.add(alert.createdTime.lt(getBeforeTime())); } if (!isIncludeCleared()) { preds.add(alert.cleared.isFalse()); } if (getTypes().size() > 0) { BooleanBuilder likes = new BooleanBuilder(); getTypes().stream() .map(uri -> alert.typeString.like(uri.toASCIIString().concat("%"))) .forEach(pred -> likes.or(pred)); preds.add(likes); } // When limiting and using "after" criteria only, we need to sort ascending to get the next n values after the given id/time. // In all other cases sort descending. The results will be ordered correctly when aggregated by the provider. if (getLimit() != Integer.MAX_VALUE && getAfterTime() != null && getBeforeTime() == null) { query.orderBy(alert.createdTime.asc()); } else { query.orderBy(alert.createdTime.desc()); } if (preds.isEmpty()) { return query; } else { return query.where(preds.toArray(new Predicate[preds.size()])); } } } }