content: /**
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You 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.
   */
  package org.apache.activemq.broker.scheduler;
  
  import java.io.IOException;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  import jakarta.jms.MessageFormatException;
  
  import org.apache.activemq.ScheduledMessage;
  import org.apache.activemq.advisory.AdvisorySupport;
  import org.apache.activemq.broker.Broker;
  import org.apache.activemq.broker.BrokerFilter;
  import org.apache.activemq.broker.BrokerService;
  import org.apache.activemq.broker.Connection;
  import org.apache.activemq.broker.ConnectionContext;
  import org.apache.activemq.broker.Connector;
  import org.apache.activemq.broker.ProducerBrokerExchange;
  import org.apache.activemq.broker.region.ConnectionStatistics;
  import org.apache.activemq.command.ActiveMQDestination;
  import org.apache.activemq.command.Command;
  import org.apache.activemq.command.ConnectionControl;
  import org.apache.activemq.command.ExceptionResponse;
  import org.apache.activemq.command.Message;
  import org.apache.activemq.command.MessageId;
  import org.apache.activemq.command.ProducerId;
  import org.apache.activemq.command.ProducerInfo;
  import org.apache.activemq.command.Response;
  import org.apache.activemq.openwire.OpenWireFormat;
  import org.apache.activemq.security.SecurityContext;
  import org.apache.activemq.state.ProducerState;
  import org.apache.activemq.transaction.Synchronization;
  import org.apache.activemq.usage.JobSchedulerUsage;
  import org.apache.activemq.usage.SystemUsage;
  import org.apache.activemq.util.ByteSequence;
  import org.apache.activemq.util.IdGenerator;
  import org.apache.activemq.util.LongSequenceGenerator;
  import org.apache.activemq.util.TypeConversionSupport;
  import org.apache.activemq.wireformat.WireFormat;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  public class SchedulerBroker extends BrokerFilter implements JobListener {
      private static final Logger LOG = LoggerFactory.getLogger(SchedulerBroker.class);
      private static final IdGenerator ID_GENERATOR = new IdGenerator();
      private static final LongSequenceGenerator longGenerator = new LongSequenceGenerator();
      /**
       * The max repeat value allowed to prevent clients from causing DoS issues with huge repeat counts
       */
      private static final int MAX_REPEAT_ALLOWED = 1000;
      private final LongSequenceGenerator messageIdGenerator = new LongSequenceGenerator();
      private final AtomicBoolean started = new AtomicBoolean();
      private final WireFormat wireFormat = new OpenWireFormat();
      private final ConnectionContext context = new ConnectionContext();
      private final ProducerId producerId = new ProducerId();
      private final SystemUsage systemUsage;
  
      private final JobSchedulerStore store;
      private JobScheduler scheduler;
      private int maxRepeatAllowed = MAX_REPEAT_ALLOWED;
  
      public SchedulerBroker(BrokerService brokerService, Broker next, JobSchedulerStore store) throws Exception {
          super(next);
  
          this.store = store;
          this.producerId.setConnectionId(ID_GENERATOR.generateId());
          this.context.setSecurityContext(SecurityContext.BROKER_SECURITY_CONTEXT);
          // we only get response on unexpected error
          this.context.setConnection(new Connection() {
  
              private final long connectedTimestamp = System.currentTimeMillis();
  
              @Override
              public Connector getConnector() {
                  return null;
              }
  
              @Override
              public void dispatchSync(Command message) {
                  if (message instanceof ExceptionResponse) {
                      LOG.warn("Unexpected response: {}", message);
                  }
              }
  
              @Override
              public void dispatchAsync(Command command) {
                  if (command instanceof ExceptionResponse) {
                      LOG.warn("Unexpected response: {}", command);
                  }
              }
  
              @Override
              public Response service(Command command) {
                  return null;
              }
  
              @Override
              public void serviceException(Throwable error) {
                  LOG.warn("Unexpected exception", error);
              }
  
              @Override
              public boolean isSlow() {
                  return false;
              }
  
              @Override
              public boolean isBlocked() {
                  return false;
              }
  
              @Override
              public boolean isConnected() {
                  return false;
              }
  
              @Override
              public boolean isActive() {
                  return false;
              }
  
              @Override
              public int getDispatchQueueSize() {
                  return 0;
              }
  
              @Override
              public ConnectionStatistics getStatistics() {
                  return null;
              }
  
              @Override
              public boolean isManageable() {
                  return false;
              }
  
              @Override
              public String getRemoteAddress() {
                  return null;
              }
  
              @Override
              public void serviceExceptionAsync(IOException e) {
                  LOG.warn("Unexpected async ioexception", e);
              }
  
              @Override
              public String getConnectionId() {
                  return null;
              }
  
              @Override
              public boolean isNetworkConnection() {
                  return false;
              }
  
              @Override
              public boolean isFaultTolerantConnection() {
                  return false;
              }
  
              @Override
              public void updateClient(ConnectionControl control) {}
  
              @Override
              public int getActiveTransactionCount() {
                  return 0;
              }
  
              @Override
              public Long getOldestActiveTransactionDuration() {
                  return null;
              }
  
              
              @Override
              public Long getConnectedTimestamp() {
                  return connectedTimestamp;
              }
  
              @Override
              public void start() throws Exception {}
  
              @Override
              public void stop() throws Exception {}
          });
          this.context.setBroker(next);
          this.systemUsage = brokerService.getSystemUsage();
  
          wireFormat.setVersion(brokerService.getStoreOpenWireVersion());
      }
  
      public synchronized JobScheduler getJobScheduler() throws Exception {
          return new JobSchedulerFacade(this);
      }
  
      @Override
      public void start() throws Exception {
          this.started.set(true);
          getInternalScheduler();
          super.start();
      }
  
      @Override
      public void stop() throws Exception {
          if (this.started.compareAndSet(true, false)) {
  
              if (this.store != null) {
                  this.store.stop();
              }
              if (this.scheduler != null) {
                  this.scheduler.removeListener(this);
                  this.scheduler = null;
              }
          }
          super.stop();
      }
  
      @Override
      public void send(ProducerBrokerExchange producerExchange, final Message messageSend) throws Exception {
          ConnectionContext context = producerExchange.getConnectionContext();
  
          final String jobId = (String) messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_ID);
          final Object cronValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_CRON);
          final Object periodValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD);
          final Object delayValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY);
  
          String physicalName = messageSend.getDestination().getPhysicalName();
          boolean schedulerManage = physicalName.regionMatches(true, 0, ScheduledMessage.AMQ_SCHEDULER_MANAGEMENT_DESTINATION, 0,
              ScheduledMessage.AMQ_SCHEDULER_MANAGEMENT_DESTINATION.length());
  
          if (schedulerManage == true) {
  
              JobScheduler scheduler = getInternalScheduler();
              ActiveMQDestination replyTo = messageSend.getReplyTo();
  
              String action = (String) messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION);
  
              if (action != null) {
  
                  Object startTime = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION_START_TIME);
                  Object endTime = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULER_ACTION_END_TIME);
  
                  if (replyTo != null && action.equals(ScheduledMessage.AMQ_SCHEDULER_ACTION_BROWSE)) {
  
                      if (startTime != null && endTime != null) {
  
                          long start = (Long) TypeConversionSupport.convert(startTime, Long.class);
                          long finish = (Long) TypeConversionSupport.convert(endTime, Long.class);
  
                          for (Job job : scheduler.getAllJobs(start, finish)) {
                              sendScheduledJob(producerExchange.getConnectionContext(), job, replyTo);
                          }
                      } else {
                          for (Job job : scheduler.getAllJobs()) {
                              sendScheduledJob(producerExchange.getConnectionContext(), job, replyTo);
                          }
                      }
                  }
                  if (jobId != null && action.equals(ScheduledMessage.AMQ_SCHEDULER_ACTION_REMOVE)) {
                      scheduler.remove(jobId);
                  } else if (action.equals(ScheduledMessage.AMQ_SCHEDULER_ACTION_REMOVEALL)) {
  
                      if (startTime != null && endTime != null) {
  
                          long start = (Long) TypeConversionSupport.convert(startTime, Long.class);
                          long finish = (Long) TypeConversionSupport.convert(endTime, Long.class);
  
                          scheduler.removeAllJobs(start, finish);
                      } else {
                          scheduler.removeAllJobs();
                      }
                  }
              }
  
          } else if ((cronValue != null || periodValue != null || delayValue != null) && jobId == null) {
  
              // Check for room in the job scheduler store
              if (systemUsage.getJobSchedulerUsage() != null) {
                  JobSchedulerUsage usage = systemUsage.getJobSchedulerUsage();
                  if (usage.isFull()) {
                      final String logMessage = "Job Scheduler Store is Full (" +
                          usage.getPercentUsage() + "% of " + usage.getLimit() +
                          "). Stopping producer (" + messageSend.getProducerId() +
                          ") to prevent flooding of the job scheduler store." +
                          " See http://activemq.apache.org/producer-flow-control.html for more info";
  
                      long start = System.currentTimeMillis();
                      long nextWarn = start;
                      while (!usage.waitForSpace(1000)) {
                          if (context.getStopping().get()) {
                              throw new IOException("Connection closed, send aborted.");
                          }
  
                          long now = System.currentTimeMillis();
                          if (now >= nextWarn) {
                              LOG.info("{}: {} (blocking for: {}s)", usage, logMessage, (now - start) / 1000);
                              nextWarn = now + 30000l;
                          }
                      }
                  }
              }
  
              if (context.isInTransaction()) {
                  context.getTransaction().addSynchronization(new Synchronization() {
                      @Override
                      public void afterCommit() throws Exception {
                          doSchedule(messageSend, cronValue, periodValue, delayValue);
                      }
                  });
              } else {
                  doSchedule(messageSend, cronValue, periodValue, delayValue);
              }
          } else {
              super.send(producerExchange, messageSend);
          }
      }
  
      private void doSchedule(Message messageSend, Object cronValue, Object periodValue, Object delayValue) throws Exception {
          long delay = 0;
          long period = 0;
          int repeat = 0;
          String cronEntry = "";
  
          // clear transaction context
          Message msg = messageSend.copy();
          msg.setTransactionId(null);
          org.apache.activemq.util.ByteSequence packet = wireFormat.marshal(msg);
          if (cronValue != null) {
              cronEntry = cronValue.toString();
          }
          if (periodValue != null) {
              period = (Long) TypeConversionSupport.convert(periodValue, Long.class);
          }
          if (delayValue != null) {
              delay = (Long) TypeConversionSupport.convert(delayValue, Long.class);
          }
          Object repeatValue = msg.getProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT);
          if (repeatValue != null) {
              repeat = (Integer) TypeConversionSupport.convert(repeatValue, Integer.class);
              if (repeat > maxRepeatAllowed) {
                  throw new MessageFormatException("The scheduled repeat value is too large");
              }
          }
  
          //job id should be unique for every job (Same format as MessageId)
          MessageId jobId = new MessageId(messageSend.getMessageId().getProducerId(), longGenerator.getNextSequenceId());
  
          getInternalScheduler().schedule(jobId.toString(),
                  new ByteSequence(packet.data, packet.offset, packet.length), cronEntry, delay, period, repeat);
      }
  
      @Override
      public void scheduledJob(String id, ByteSequence job) {
          org.apache.activemq.util.ByteSequence packet = new org.apache.activemq.util.ByteSequence(job.getData(), job.getOffset(), job.getLength());
          try {
              Message messageSend = (Message) wireFormat.unmarshal(packet);
              messageSend.setOriginalTransactionId(null);
              Object repeatValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT);
              Object cronValue = messageSend.getProperty(ScheduledMessage.AMQ_SCHEDULED_CRON);
              String cronStr = cronValue != null ? cronValue.toString() : null;
              int repeat = 0;
              if (repeatValue != null) {
                  repeat = (Integer) TypeConversionSupport.convert(repeatValue, Integer.class);
                  if (repeat > maxRepeatAllowed) {
                      throw new MessageFormatException("The scheduled repeat value is too large");
                  }
              }
  
              if (repeat != 0 || cronStr != null && cronStr.length() > 0) {
                  // create a unique id - the original message could be sent
                  // lots of times
                  messageSend.setMessageId(new MessageId(producerId, messageIdGenerator.getNextSequenceId()));
              }
  
              // Add the jobId as a property
              messageSend.setProperty("scheduledJobId", id);
  
              // if this goes across a network - we don't want it rescheduled
              messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD);
              messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY);
              messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT);
              messageSend.removeProperty(ScheduledMessage.AMQ_SCHEDULED_CRON);
  
              if (messageSend.getTimestamp() > 0 && messageSend.getExpiration() > 0) {
  
                  long oldExpiration = messageSend.getExpiration();
                  long newTimeStamp = System.currentTimeMillis();
                  long timeToLive = 0;
                  long oldTimestamp = messageSend.getTimestamp();
  
                  if (oldExpiration > 0) {
                      timeToLive = oldExpiration - oldTimestamp;
                  }
  
                  long expiration = timeToLive + newTimeStamp;
  
                  if (expiration > oldExpiration) {
                      if (timeToLive > 0 && expiration > 0) {
                          messageSend.setExpiration(expiration);
                      }
                      messageSend.setTimestamp(newTimeStamp);
                      LOG.debug("Set message {} timestamp from {} to {}",
                              messageSend.getMessageId(), oldTimestamp, newTimeStamp);
                  }
              }
  
              // Repackage the message contents prior to send now that all updates are complete.
              messageSend.beforeMarshall(wireFormat);
  
              final ProducerBrokerExchange producerExchange = new ProducerBrokerExchange();
              producerExchange.setConnectionContext(context);
              producerExchange.setMutable(true);
              producerExchange.setProducerState(new ProducerState(new ProducerInfo()));
              super.send(producerExchange, messageSend);
          } catch (Exception e) {
              LOG.error("Failed to send scheduled message {}", id, e);
          }
      }
  
      protected synchronized JobScheduler getInternalScheduler() throws Exception {
          if (this.started.get()) {
              if (this.scheduler == null && store != null) {
                  this.scheduler = store.getJobScheduler("JMS");
                  this.scheduler.addListener(this);
                  this.scheduler.startDispatching();
              }
              return this.scheduler;
          }
          return null;
      }
  
      protected void sendScheduledJob(ConnectionContext context, Job job, ActiveMQDestination replyTo) throws Exception {
  
          org.apache.activemq.util.ByteSequence packet = new org.apache.activemq.util.ByteSequence(job.getPayload());
          try {
              Message msg = (Message) this.wireFormat.unmarshal(packet);
              msg.setOriginalTransactionId(null);
              msg.setPersistent(false);
              msg.setType(AdvisorySupport.ADIVSORY_MESSAGE_TYPE);
              msg.setMessageId(new MessageId(this.producerId, this.messageIdGenerator.getNextSequenceId()));
  
              // Preserve original destination
              msg.setOriginalDestination(msg.getDestination());
  
              msg.setDestination(replyTo);
              msg.setResponseRequired(false);
              msg.setProducerId(this.producerId);
  
              // Add the jobId as a property
              msg.setProperty("scheduledJobId", job.getJobId());
  
              final boolean originalFlowControl = context.isProducerFlowControl();
              final ProducerBrokerExchange producerExchange = new ProducerBrokerExchange();
              producerExchange.setConnectionContext(context);
              producerExchange.setMutable(true);
              producerExchange.setProducerState(new ProducerState(new ProducerInfo()));
              try {
                  context.setProducerFlowControl(false);
                  this.next.send(producerExchange, msg);
              } finally {
                  context.setProducerFlowControl(originalFlowControl);
              }
          } catch (Exception e) {
              LOG.error("Failed to send scheduled message {}", job.getJobId(), e);
          }
      }
  
      public int getMaxRepeatAllowed() {
          return maxRepeatAllowed;
      }
  
      public void setMaxRepeatAllowed(int maxRepeatAllowed) {
          this.maxRepeatAllowed = maxRepeatAllowed;
      }
  }
  