Package hudson.plugins.im

Source Code of hudson.plugins.im.IMPublisher

package hudson.plugins.im;

import hudson.Launcher;
import hudson.Util;
import hudson.matrix.MatrixAggregatable;
import hudson.matrix.MatrixAggregator;
import hudson.matrix.MatrixConfiguration;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixProject;
import hudson.model.BuildListener;
import hudson.model.UserProperty;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.User;
import hudson.plugins.im.build_notify.BuildToChatNotifier;
import hudson.plugins.im.build_notify.DefaultBuildToChatNotifier;
import hudson.plugins.im.tools.BuildHelper;
import hudson.plugins.im.tools.ExceptionHelper;
import hudson.plugins.im.tools.BuildHelper.ExtResult;
import hudson.scm.ChangeLogSet;
import hudson.scm.ChangeLogSet.Entry;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.springframework.util.Assert;

import com.google.common.collect.Lists;

/**
* The actual Publisher which sends notification messages out to the clients.
*
* @author Uwe Schaefer
* @author Christoph Kutzinski
*/
public abstract class IMPublisher extends Notifier implements BuildStep, MatrixAggregatable
{
  private static final Logger LOGGER = Logger.getLogger(IMPublisher.class.getName());
 
    private List<IMMessageTarget> targets;
   
    /**
     * @deprecated only left here to deserialize old configs
     */
    @Deprecated
  private hudson.plugins.jabber.NotificationStrategy notificationStrategy;
   
    private NotificationStrategy strategy;
    private final boolean notifyOnBuildStart;
    private final boolean notifySuspects;
    private final boolean notifyCulprits;
    private final boolean notifyFixers;
    private final boolean notifyUpstreamCommitters;
    private BuildToChatNotifier buildToChatNotifier;
    private MatrixJobMultiplier matrixMultiplier = MatrixJobMultiplier.ONLY_CONFIGURATIONS;
   
    /**
     * @deprecated Only for deserializing old instances
     */
    @SuppressWarnings("unused")
    @Deprecated
    private transient String defaultIdSuffix;

    /**
     * @deprecated
     *      as of 1.9. Use {@link #IMPublisher(List, String, boolean, boolean, boolean, boolean, boolean, BuildToChatNotifier)}
     *      instead.
     */
    @Deprecated
  protected IMPublisher(List<IMMessageTarget> defaultTargets,
        String notificationStrategyString,
        boolean notifyGroupChatsOnBuildStart,
        boolean notifySuspects,
        boolean notifyCulprits,
        boolean notifyFixers,
        boolean notifyUpstreamCommitters) {
        this(defaultTargets,notificationStrategyString,notifyGroupChatsOnBuildStart,notifySuspects,notifyCulprits,
                notifyFixers,notifyUpstreamCommitters,new DefaultBuildToChatNotifier(), MatrixJobMultiplier.ALL);
    }

    protected IMPublisher(List<IMMessageTarget> defaultTargets,
        String notificationStrategyString,
        boolean notifyGroupChatsOnBuildStart,
        boolean notifySuspects,
        boolean notifyCulprits,
        boolean notifyFixers,
        boolean notifyUpstreamCommitters,
            BuildToChatNotifier buildToChatNotifier,
            MatrixJobMultiplier matrixMultiplier)
    {
      if (defaultTargets != null) {
        this.targets = defaultTargets;
      } else {
        this.targets = Collections.emptyList();
      }
     
        NotificationStrategy strategy = NotificationStrategy.forDisplayName(notificationStrategyString);
        if (strategy == null) {
          strategy = NotificationStrategy.STATECHANGE_ONLY;
        }
        this.strategy = strategy;
       
        this.notifyOnBuildStart = notifyGroupChatsOnBuildStart;
        this.notifySuspects = notifySuspects;
        this.notifyCulprits = notifyCulprits;
        this.notifyFixers = notifyFixers;
        this.notifyUpstreamCommitters = notifyUpstreamCommitters;
        this.buildToChatNotifier = buildToChatNotifier;
        this.matrixMultiplier = matrixMultiplier;
    }
   
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean needsToRunAfterFinalized() {
        // notifyUpstreamCommitters needs the fingerprints to be generated
        // which seems to happen quite late in the build
        return this.notifyUpstreamCommitters;
    }
   
    /**
     * Returns a short name of the plugin to be used e.g. in log messages.
     */
    protected abstract String getPluginName();
   
    protected abstract IMConnection getIMConnection() throws IMException;

    protected NotificationStrategy getNotificationStrategy() {
        return strategy;
    }
   
    protected void setNotificationStrategy(NotificationStrategy strategy) {
      this.strategy = strategy;
    }

    public BuildToChatNotifier getBuildToChatNotifier() {
        return buildToChatNotifier;
    }

    /**
     * Returns the notification targets configured on a per-job basis.
     */
    public List<IMMessageTarget> getNotificationTargets() {
        return this.targets;
    }
   
    /**
     * Returns the notification target which should actually be used for notification.
     *
     * Differs from {@link #getNotificationTargets()} because it also takes
     * {@link IMPublisherDescriptor#getDefaultTargets()} into account!
     */
    protected List<IMMessageTarget> calculateTargets() {
      if (getNotificationTargets() != null && getNotificationTargets().size() > 0) {
        return getNotificationTargets();
      }
     
      return ((IMPublisherDescriptor)getDescriptor()).getDefaultTargets();
    }

    /**
     * Returns the notification targets as a string suitable for
     * display in the settings page.
     *
     * Returns an empty string if no targets are set.
     */
    public String getTargets() {
      if (this.targets == null) {
        return "";
      }

        final StringBuilder sb = new StringBuilder();
        for (final IMMessageTarget t : this.targets) {
            sb.append(getIMDescriptor().getIMMessageTargetConverter().toString(t));
            sb.append(" ");
        }
        return sb.toString().trim();
    }
 
    @Deprecated
    protected void setTargets(String targetsAsString) throws IMMessageTargetConversionException {
      this.targets = new LinkedList<IMMessageTarget>();
     
        final String[] split = targetsAsString.split("\\s");
        final IMMessageTargetConverter conv = getIMDescriptor().getIMMessageTargetConverter();
        for (final String fragment : split)
        {
            IMMessageTarget createIMMessageTarget;
            createIMMessageTarget = conv.fromString(fragment);
            if (createIMMessageTarget != null)
            {
                this.targets.add(createIMMessageTarget);
            }
        }
  }
   
    /**
     * @deprecated Should only be used to deserialize old instances
     */
    @Deprecated
  protected void setNotificationTargets(List<IMMessageTarget> targets) {
      if (targets != null) {
        this.targets = targets;
      } else {
        this.targets = Collections.emptyList();
      }
    }
   
    /**
     * Returns the selected notification strategy as a string
     * suitable for display.
     */
    public final String getStrategy() {
        return getNotificationStrategy().getDisplayName();
    }
   
    /**
     * Specifies if the starting of builds should be notified to
     * the registered chat rooms.
     */
    public boolean getNotifyOnStart() {
      return notifyOnBuildStart;
    }
   
    /**
     * Specifies if committers to failed builds should be informed about
     * build failures.
     */
    public final boolean getNotifySuspects() {
      return notifySuspects;
    }
   
    /**
     * Specifies if culprits - i.e. committers to previous already failing
     * builds - should be informed about subsequent build failures.
     */
    public final boolean getNotifyCulprits() {
      return notifyCulprits;
    }

    /**
     * Specifies if 'fixers' should be informed about
     * fixed builds.
     */
    public final boolean getNotifyFixers() {
      return notifyFixers;
    }
   
    /**
     * Specifies if upstream committers should be informed about
     * build failures.
     */
    public final boolean getNotifyUpstreamCommitters() {
        return notifyUpstreamCommitters;
    }
   
    /**
     * Logs message to the build listener's logger.
     */
    protected void log(BuildListener listener, String message) {
      listener.getLogger().append(getPluginName()).append(": ").append(message).append("\n");
    }

    @Override
    public boolean perform(final AbstractBuild<?,?> build, final Launcher launcher, final BuildListener buildListener)
            throws InterruptedException, IOException {
        Assert.notNull(build, "Parameter 'build' must not be null.");
        Assert.notNull(buildListener, "Parameter 'buildListener' must not be null.");
       
        if (build.getProject() instanceof MatrixConfiguration) {
            if (getMatrixNotifier() == MatrixJobMultiplier.ONLY_CONFIGURATIONS
                || getMatrixNotifier() == MatrixJobMultiplier.ALL) {
                notifyOnBuildEnd(build, buildListener);
            }
        } else {
            notifyOnBuildEnd(build, buildListener);
        }
       
        return true;
    }

    /**
     * Sends notification at build end including maybe notifications of culprits, fixers or so.
     */
    /* package for testing */ void notifyOnBuildEnd(final AbstractBuild<?, ?> build,
            final BuildListener buildListener) throws IOException,
            InterruptedException {
        if (getNotificationStrategy().notificationWanted(build)) {
            notifyChatsOnBuildEnd(build, buildListener);
        }

        if (BuildHelper.isStillFailureOrUnstable(build) || BuildHelper.getExtendedResult(build) == ExtResult.NOW_UNSTABLE) {
            if (this.notifySuspects) {
              log(buildListener, "Notifying suspects");
              final String message = getBuildToChatNotifier().suspectMessage(this, build, buildListener, false);
             
              for (IMMessageTarget target : calculateIMTargets(getCommitters(build), buildListener)) {
                try {
                  log(buildListener, "Sending notification to suspect: " + target.toString());
                  sendNotification(message, target, buildListener);
                } catch (final Throwable e) {
                  log(buildListener, "There was an error sending suspect notification to: " + target.toString());
                }
              }
            }
           
            if (this.notifyCulprits) {
              log(buildListener, "Notifying culprits");
              final String message = getBuildToChatNotifier().culpritMessage(this, build, buildListener);
             
              for (IMMessageTarget target : calculateIMTargets(getCulpritsOnly(build), buildListener)) {
                try {
                  log(buildListener, "Sending notification to culprit: " + target.toString());
                  sendNotification(message, target, buildListener);
                } catch (final Throwable e) {
                  log(buildListener, "There was an error sending culprit notification to: " + target.toString());
                }
              }
            }
        } else if (BuildHelper.isFailureOrUnstable(build)) {
            boolean committerNotified = false;
            if (this.notifySuspects) {
                log(buildListener, "Notifying suspects");
                String message = getBuildToChatNotifier().suspectMessage(this, build, buildListener, true);
               
                for (IMMessageTarget target : calculateIMTargets(getCommitters(build), buildListener)) {
                    try {
                        log(buildListener, "Sending notification to suspect: " + target.toString());
                        sendNotification(message, target, buildListener);
                        committerNotified = true;
                    } catch (final Throwable e) {
                        log(buildListener, "There was an error sending suspect notification to: " + target.toString());
                    }
                }
            }
           
            if (this.notifyUpstreamCommitters && !committerNotified) {
                notifyUpstreamCommitters(build, buildListener);
            }
        }
       
        if (this.notifyFixers && BuildHelper.isFix(build)) {
          buildListener.getLogger().append("Notifying fixers\n");
          final String message = getBuildToChatNotifier().fixerMessage(this, build, buildListener);
         
          for (IMMessageTarget target : calculateIMTargets(getCommitters(build), buildListener)) {
            try {
              log(buildListener, "Sending notification to fixer: " + target.toString());
              sendNotification(message, target, buildListener);
            } catch (final Throwable e) {
              log(buildListener, "There was an error sending fixer notification to: " + target.toString());
            }
          }
        }
    }

  private void sendNotification(String message, IMMessageTarget target, BuildListener buildListener)
      throws IMException {
    IMConnection imConnection = getIMConnection();
    if (imConnection instanceof DummyConnection) {
      // quite hacky
      log(buildListener, "[ERROR] not connected. Cannot send message to '" + target + "'");
    } else {
      getIMConnection().send(target, message);
    }
  }

    /**
     * Looks for committers in the direct upstream builds and notifies them.
     * If no committers are found in the immediate upstream builds, then look one level higher.
     * Repeat until a committer is found or no more upstream builds are found.
     */
  private void notifyUpstreamCommitters(final AbstractBuild<?, ?> build,
      final BuildListener buildListener) {
       
        Map<User, AbstractBuild<?,?>> committers = getNearestUpstreamCommitters(build);
             
       
        for (Map.Entry<User, AbstractBuild<?, ?>> entry : committers.entrySet()) {
            String message = getBuildToChatNotifier().upstreamCommitterMessage(this, build, buildListener, entry.getValue());
           
            IMMessageTarget target = calculateIMTarget(entry.getKey(), buildListener);
            try {
                log(buildListener, "Sending notification to upstream committer: " + target.toString());
                sendNotification(message, target, buildListener);
            } catch (final Throwable e) {
                log(buildListener, "There was an error sending upstream committer notification to: " + target.toString());
            }
        }
  }
   
  /**
     * Looks for committers in the direct upstream builds.
     * If no committers are found in the immediate upstream builds, then look one level higher.
     * Repeat until a committer is found or no more upstream builds are found.
     */
    @SuppressWarnings("rawtypes")
    Map<User, AbstractBuild<?,?>> getNearestUpstreamCommitters(AbstractBuild<?, ?> build) {
        Map<AbstractProject, List<AbstractBuild>> upstreamBuilds = getUpstreamBuildsSinceLastStable(build);
        Map<User, AbstractBuild<?,?>> upstreamCommitters = new HashMap<User, AbstractBuild<?,?>>();
       
        while (upstreamCommitters.isEmpty() && !upstreamBuilds.isEmpty()) {
            Map<AbstractProject, List<AbstractBuild>> currentLevel = upstreamBuilds;
            // new map for the builds one level higher up:
            upstreamBuilds = new HashMap<AbstractProject, List<AbstractBuild>>();
           
            for (Map.Entry<AbstractProject, List<AbstractBuild>> entry : currentLevel.entrySet()) {
                List<AbstractBuild> upstreams = entry.getValue();
               
                for (AbstractBuild upstreamBuild : upstreams) {
               
                    if (upstreamBuild != null) {
                       
                        if (! downstreamIsFirstInRangeTriggeredByUpstream(upstreamBuild, build)) {
                            continue;
                        }
                       
                        Set<User> committers = getCommitters(upstreamBuild);
                        for (User committer : committers) {
                            upstreamCommitters.put(committer, upstreamBuild);
                        }
                       
                        upstreamBuilds.putAll(getUpstreamBuildsSinceLastStable(upstreamBuild));
                    }
                }
            }
        }
       
        return upstreamCommitters;
    }
   
    @SuppressWarnings("rawtypes")
    private Map<AbstractProject, List<AbstractBuild>> getUpstreamBuildsSinceLastStable(AbstractBuild<?,?> currentBuild) {
      // may be null:
      AbstractBuild<?, ?> previousSuccessfulBuild = currentBuild.getPreviousSuccessfulBuild();
     
      if (previousSuccessfulBuild == null) {
          return Collections.emptyMap();
      }
     
      Map<AbstractProject, List<AbstractBuild>> result = new HashMap<AbstractProject, List<AbstractBuild>>();
     
     
      Set<AbstractProject> upstreamProjects = currentBuild.getUpstreamBuilds().keySet();
     
      for (AbstractProject upstreamProject : upstreamProjects) {
          result.put(upstreamProject,
                  getUpstreamBuilds(upstreamProject, previousSuccessfulBuild, currentBuild));
      }
     
      return result;
    }

    /**
     * Gets all upstream builds for a given upstream project and a given downstream since/until build pair
     *
     * @param upstreamProject the upstream project
     * @param sinceBuild the downstream build since when to get the upstream builds (exclusive)
     * @param untilBuild the downstream build until when to get the upstream builds (inclusive)
     * @return the upstream builds. May be empty but never null
     */
    @SuppressWarnings("rawtypes")
    private List<AbstractBuild> getUpstreamBuilds(
            AbstractProject upstreamProject,
            AbstractBuild<?, ?> sinceBuild,
            AbstractBuild<?, ?> untilBuild) {
       
        List<AbstractBuild> result = Lists.newArrayList();
       
        AbstractBuild<?, ?> sinceBuildUpstreamBuild = sinceBuild.getUpstreamRelationshipBuild(upstreamProject);
        AbstractBuild<?, ?> untilBuildUpstreamBuild = untilBuild.getUpstreamRelationshipBuild(upstreamProject);
       
        AbstractBuild<?, ?> build = sinceBuildUpstreamBuild;
       
        if (build == null) {
          return result;
        }
       
        do {
            build = build.getNextBuild();
           
            if (build != null) {
                result.add(build);
            }
           
        } while (build != untilBuildUpstreamBuild && build != null);
       
        return result;
    }

    /**
     * Determines if downstreamBuild is the 1st build of the downstream project
     * which has a dependency to the upstreamBuild.
     */
    //@Bug(6712)
    private boolean downstreamIsFirstInRangeTriggeredByUpstream(
            AbstractBuild<?, ?> upstreamBuild, AbstractBuild<?, ?> downstreamBuild) {
        RangeSet rangeSet = upstreamBuild.getDownstreamRelationship(downstreamBuild.getProject());
       
        if (rangeSet.isEmpty()) {
            // should not happen
            LOGGER.warning("Range set is empty. Upstream " + upstreamBuild + ", downstream " + downstreamBuild);
            return false;
        }
       
        if (rangeSet.min() == downstreamBuild.getNumber()) {
            return true;
        }
        return false;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean prebuild(AbstractBuild<?, ?> build, BuildListener buildListener) {
        if (getNotifyOnStart()) {
           if (build.getProject() instanceof MatrixConfiguration) {
               if (getMatrixNotifier() == MatrixJobMultiplier.ONLY_CONFIGURATIONS
                       || getMatrixNotifier() == MatrixJobMultiplier.ALL) {
                   notifyChatsOnBuildStart(build, buildListener);
               }
           } else {
               notifyChatsOnBuildStart(build, buildListener);
           }
        }
        return true;
    }
   
    /**
     * Notify all registered chats about the build start.
     * When the start message is null or empty, no message is sent.
     */
    /* package for testing */ void notifyChatsOnBuildStart(AbstractBuild<?, ?> build, BuildListener buildListener) {
        try {
            final String msg = buildToChatNotifier.buildStartMessage(this,build,buildListener);
            if (Util.fixEmpty(msg) == null) {
                return;
            }
            for (final IMMessageTarget target : calculateTargets()) {
                // only notify group chats
                if (target instanceof GroupChatIMMessageTarget) {
                    try {
                        sendNotification(msg, target, buildListener);
                    } catch (final Throwable e) {
                        log(buildListener, "There was an error sending notification to: " + target.toString());
                    }
                }
            }
        } catch (Throwable t) {
            // ignore: never, ever cancel a build because a notification fails
            log(buildListener, "There was an error in the IM plugin: " + ExceptionHelper.dump(t));
        }
    }
   
    /**
     * Notify all registered chats about the build result.
     * When the completion message is null or empty, no message is sent.
     */
  private void notifyChatsOnBuildEnd(final AbstractBuild<?, ?> build, final BuildListener buildListener) throws IOException, InterruptedException {
        String msg = buildToChatNotifier.buildCompletionMessage(this,build,buildListener);
        if (Util.fixEmpty(msg) == null)  {
            return;
        }
    for (IMMessageTarget target : calculateTargets())
    {
        try {
            log(buildListener, "Sending notification to: " + target.toString());
            sendNotification(msg, target, buildListener);
        } catch (final Throwable t) {
            log(buildListener, "There was an error sending notification to: " + target.toString() + "\n" + ExceptionHelper.dump(t));
        }
    }
  }
 
  private static Set<User> getCommitters(AbstractBuild<?, ?> build) {
    Set<User> committers = new HashSet<User>();
    ChangeLogSet<? extends Entry> changeSet = build.getChangeSet();
    for (Entry entry : changeSet) {
      committers.add(entry.getAuthor());
    }
    return committers;
  }
 
  /**
   * Returns the culprits WITHOUT the committers to the current build.
   */
  private static Set<User> getCulpritsOnly(AbstractBuild<?, ?> build) {
    Set<User> culprits = new HashSet<User>(build.getCulprits());
    culprits.removeAll(getCommitters(build));
    return culprits;
  }
 
  private Collection<IMMessageTarget> calculateIMTargets(Set<User> targets, BuildListener listener) {
    Set<IMMessageTarget> suspects = new HashSet<IMMessageTarget>();
   
    String defaultIdSuffix = ((IMPublisherDescriptor)getDescriptor()).getDefaultIdSuffix();
    LOGGER.fine("Default Suffix: " + defaultIdSuffix);
   
    for (User target : targets) {
        IMMessageTarget imTarget = calculateIMTarget(target, listener);
        if (imTarget != null) {
            suspects.add(imTarget);
        }
    }
    return suspects;
  }

    private IMMessageTarget calculateIMTarget(User target, BuildListener listener) {
       
        String defaultIdSuffix = ((IMPublisherDescriptor)getDescriptor()).getDefaultIdSuffix();
       
        LOGGER.fine("Possible target: " + target.getId());
        String imId = getConfiguredIMId(target);
        if (imId == null && defaultIdSuffix != null) {
            imId = target.getId() + defaultIdSuffix;
        }

        if (imId != null) {
            try {
                return getIMDescriptor().getIMMessageTargetConverter().fromString(imId);
            } catch (final IMMessageTargetConversionException e) {
                log(listener, "Invalid IM ID: " + imId);
            }
        } else {
          log(listener, "No IM ID found for: " + target.getId());
        }
       
        return null;
    }

  /**
   * {@inheritDoc}
   */
    @Override
    public abstract BuildStepDescriptor<Publisher> getDescriptor();

    /**
     * {@inheritDoc}
     */
    @Override
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.BUILD;
    }
 
    // migrate old instances
    protected Object readResolve() {
      if (this.strategy == null && this.notificationStrategy != null) {
        this.strategy = NotificationStrategy.valueOf(this.notificationStrategy.name());
        this.notificationStrategy = null;
      }
        if (buildToChatNotifier == null) {
            this.buildToChatNotifier = new DefaultBuildToChatNotifier();
        }
        if (matrixMultiplier == null) {
            this.matrixMultiplier = MatrixJobMultiplier.ONLY_CONFIGURATIONS;
        }
      return this;
    }
   
    protected final IMPublisherDescriptor getIMDescriptor() {
      return (IMPublisherDescriptor) getDescriptor();
    }
   
    /**
     * Returns the instant-messaging ID which is configured for a Jenkins user
     * (e.g. via a {@link UserProperty}) or null if there's nothing configured for
     * him/her.
     */
    protected abstract String getConfiguredIMId(User user);
   
    /**
     * Specifies how many notifications to send for matrix projects.
     * Like 'only parent', 'only configurations', 'both'
     */
    public MatrixJobMultiplier getMatrixNotifier() {
        return this.matrixMultiplier;
    }
   
    public void setMatrixNotifier(MatrixJobMultiplier matrixMultiplier) {
        this.matrixMultiplier = matrixMultiplier;
    }
   
   
    public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) {
        return new MatrixAggregator(build, launcher, listener) {

            @Override
            public boolean startBuild() throws InterruptedException,
                    IOException {
                if (getNotifyOnStart()) {
                    if (getMatrixNotifier() == MatrixJobMultiplier.ALL || getMatrixNotifier() == MatrixJobMultiplier.ONLY_PARENT) {
                        notifyChatsOnBuildStart(build, listener);
                    }
                }
                return super.startBuild();
            }

            @Override
            public boolean endBuild() throws InterruptedException, IOException {
                if (getMatrixNotifier() == MatrixJobMultiplier.ALL || getMatrixNotifier() == MatrixJobMultiplier.ONLY_PARENT) {
                    notifyOnBuildEnd(build, listener);
                }
                return super.endBuild();
            }
        };
    }
   
    // Helper method for the config.jelly
    public boolean isMatrixProject(AbstractProject<?,?> project) {
        return project instanceof MatrixProject;
    }
}
TOP

Related Classes of hudson.plugins.im.IMPublisher

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.