001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.console.command;
018
019import java.util.ArrayList;
020import java.util.Iterator;
021import java.util.List;
022import java.util.StringTokenizer;
023import javax.management.MBeanServerInvocationHandler;
024import javax.management.ObjectInstance;
025import javax.management.ObjectName;
026
027import org.apache.activemq.broker.jmx.QueueViewMBean;
028import org.apache.activemq.console.util.JmxMBeansUtil;
029
030public class PurgeCommand extends AbstractJmxCommand {
031
032    protected String[] helpFile = new String[] {
033        "Task Usage: Main purge [browse-options] <destinations>",
034        "Description: Delete selected destination's messages that matches the message selector.",
035        "",
036        "Purge Options:",
037        "    --msgsel <msgsel1,msglsel2>   Add to the search list messages matched by the query similar to",
038        "                                  the messages selector format.",
039        "    --reset                       After the purge operation, reset the destination statistics.",
040        "    --jmxurl <url>                Set the JMX URL to connect to.",
041        "    --pid <pid>                   Set the pid to connect to (only on Sun JVM).",
042        "    --jmxuser <user>              Set the JMX user used for authenticating.",
043        "    --jmxpassword <password>      Set the JMX password used for authenticating.",
044        "    --jmxlocal                    Use the local JMX server instead of a remote one.",
045        "    --version                     Display the version information.",
046        "    -h,-?,--help                  Display the browse broker help information.",
047        "",
048        "Examples:",
049        "    Main purge FOO.BAR",
050        "        - Delete all the messages in queue FOO.BAR",
051
052        "    Main purge --msgsel \"JMSMessageID='*:10',JMSPriority>5\" FOO.*",
053        "        - Delete all the messages in the destinations that matches FOO.* and has a JMSMessageID in",
054        "          the header field that matches the wildcard *:10, and has a JMSPriority field > 5 in the",
055        "          queue FOO.BAR.",
056        "          SLQ92 syntax is also supported.",
057        "        * To use wildcard queries, the field must be a string and the query enclosed in ''",
058        "          Use double quotes \"\" around the entire message selector string.",
059        ""
060    };
061
062    private final List<String> queryAddObjects = new ArrayList<String>(10);
063    private final List<String> querySubObjects = new ArrayList<String>(10);
064    private boolean resetStatistics;
065
066    @Override
067    public String getName() {
068        return "purge";
069    }
070
071    @Override
072    public String getOneLineDescription() {
073        return "Delete selected destination's messages that matches the message selector";
074    }
075
076    /**
077     * Execute the purge command, which allows you to purge the messages in a
078     * given JMS destination
079     *
080     * @param tokens - command arguments
081     * @throws Exception
082     */
083    @Override
084    protected void runTask(List<String> tokens) throws Exception {
085        try {
086            // If there is no queue name specified, let's select all
087            if (tokens.isEmpty()) {
088                tokens.add("*");
089            }
090
091        // Iterate through the queue names
092        for (Iterator<String> i = tokens.iterator(); i.hasNext(); ) {
093            List queueList = JmxMBeansUtil.queryMBeans(createJmxConnection(), "type=Broker,brokerName=*,destinationType=Queue,destinationName=" + i.next());
094
095            for (Iterator j = queueList.iterator(); j.hasNext(); ) {
096                ObjectName queueName = ((ObjectInstance) j.next()).getObjectName();
097                if (queryAddObjects.isEmpty()) {
098                    purgeQueue(queueName);
099                } else {
100
101                    QueueViewMBean proxy = MBeanServerInvocationHandler.
102                            newProxyInstance(createJmxConnection(),
103                                    queueName,
104                                    QueueViewMBean.class,
105                                    true);
106                    int removed = 0;
107
108                    // AMQ-3404: We support two syntaxes for the message
109                    // selector query:
110                    // 1) AMQ specific:
111                    //    "JMSPriority>2,MyHeader='Foo'"
112                    //
113                    // 2) SQL-92 syntax:
114                    //    "(JMSPriority>2) AND (MyHeader='Foo')"
115                    //
116                    // If syntax style 1) is used, the comma separated
117                    // criterias are broken into List<String> elements.
118                    // We then need to construct the SQL-92 query out of
119                    // this list.
120
121                    String sqlQuery = convertToSQL92(queryAddObjects);
122                    removed = proxy.removeMatchingMessages(sqlQuery);
123                    context.printInfo("Removed: " + removed
124                            + " messages for message selector " + sqlQuery);
125
126                        if (resetStatistics) {
127                            proxy.resetStatistics();
128                        }
129                    }
130                }
131            }
132        } catch (Exception e) {
133            context.printException(new RuntimeException("Failed to execute purge task. Reason: " + e));
134            throw new Exception(e);
135        }
136    }
137
138
139    /**
140     * Purge all the messages in the queue
141     *
142     * @param queue - ObjectName of the queue to purge
143     * @throws Exception
144     */
145    public void purgeQueue(ObjectName queue) throws Exception {
146        context.printInfo("Purging all messages in queue: " + queue.getKeyProperty("destinationName"));
147        createJmxConnection().invoke(queue, "purge", new Object[] {}, new String[] {});
148        if (resetStatistics) {
149            createJmxConnection().invoke(queue, "resetStatistics", new Object[] {}, new String[] {});
150        }
151    }
152
153    /**
154     * Handle the --msgsel, --xmsgsel.
155     *
156     * @param token - option token to handle
157     * @param tokens - succeeding command arguments
158     * @throws Exception
159     */
160    @Override
161    protected void handleOption(String token, List<String> tokens) throws Exception {
162        // If token is an additive message selector option
163        if (token.startsWith("--msgsel")) {
164
165            // If no message selector is specified, or next token is a new
166            // option
167            if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
168                context.printException(new IllegalArgumentException("Message selector not specified"));
169                return;
170            }
171
172            StringTokenizer queryTokens = new StringTokenizer(tokens.remove(0), COMMAND_OPTION_DELIMETER);
173            while (queryTokens.hasMoreTokens()) {
174                queryAddObjects.add(queryTokens.nextToken());
175            }
176        } else if (token.startsWith("--xmsgsel")) {
177            // If token is a substractive message selector option
178
179            // If no message selector is specified, or next token is a new
180            // option
181            if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
182                context.printException(new IllegalArgumentException("Message selector not specified"));
183                return;
184            }
185
186            StringTokenizer queryTokens = new StringTokenizer(tokens.remove(0), COMMAND_OPTION_DELIMETER);
187            while (queryTokens.hasMoreTokens()) {
188                querySubObjects.add(queryTokens.nextToken());
189            }
190        } else if (token.startsWith("--reset")) {
191            resetStatistics = true;
192        } else {
193            // Let super class handle unknown option
194            super.handleOption(token, tokens);
195        }
196    }
197
198    /**
199     * Converts the message selector as provided on command line
200     * argument to activem-admin into an SQL-92 conform string.
201     * E.g.
202     *   "JMSMessageID='*:10',JMSPriority>5"
203     * gets converted into
204     *   "(JMSMessageID='%:10') AND (JMSPriority>5)"
205     *
206     * @param tokens - List of message selector query parameters
207     * @return SQL-92 string of that query.
208     */
209    public String convertToSQL92(List<String> tokens) {
210        StringBuilder selector = new StringBuilder();
211
212        boolean isFirstToken = true;
213        for (Iterator i = tokens.iterator(); i.hasNext(); ) {
214            String token = i.next().toString();
215            if (token.matches("^[^=]*='.*[\\*\\?].*'$")) {
216                token = token.replace('?', '_')
217                        .replace('*', '%')
218                        .replaceFirst("=", " LIKE ");
219            }
220            if (isFirstToken) {
221                isFirstToken = false;
222            } else {
223                selector.append(" AND ");
224            }
225            selector.append('(')
226                    .append(token)
227                    .append(')');
228        }
229        return selector.toString();
230    }
231
232    /**
233     * Print the help messages for the browse command
234     */
235    @Override
236    protected void printHelp() {
237        context.printHelp(helpFile);
238    }
239
240}