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        // If there is no queue name specified, let's select all
086        if (tokens.isEmpty()) {
087            tokens.add("*");
088        }
089
090        // Iterate through the queue names
091        for (Iterator<String> i = tokens.iterator(); i.hasNext(); ) {
092            List queueList = JmxMBeansUtil.queryMBeans(createJmxConnection(), "type=Broker,brokerName=*,destinationType=Queue,destinationName=" + i.next());
093
094            for (Iterator j = queueList.iterator(); j.hasNext(); ) {
095                ObjectName queueName = ((ObjectInstance) j.next()).getObjectName();
096                if (queryAddObjects.isEmpty()) {
097                    purgeQueue(queueName);
098                } else {
099
100                    QueueViewMBean proxy = MBeanServerInvocationHandler.
101                            newProxyInstance(createJmxConnection(),
102                                    queueName,
103                                    QueueViewMBean.class,
104                                    true);
105                    int removed = 0;
106
107                    // AMQ-3404: We support two syntaxes for the message
108                    // selector query:
109                    // 1) AMQ specific:
110                    //    "JMSPriority>2,MyHeader='Foo'"
111                    //
112                    // 2) SQL-92 syntax:
113                    //    "(JMSPriority>2) AND (MyHeader='Foo')"
114                    //
115                    // If syntax style 1) is used, the comma separated
116                    // criterias are broken into List<String> elements.
117                    // We then need to construct the SQL-92 query out of
118                    // this list.
119
120                    String sqlQuery = convertToSQL92(queryAddObjects);
121                    removed = proxy.removeMatchingMessages(sqlQuery);
122                    context.printInfo("Removed: " + removed
123                            + " messages for message selector " + sqlQuery);
124
125                    if (resetStatistics) {
126                        proxy.resetStatistics();
127                    }
128                }
129            }
130        }
131    }
132
133
134    /**
135     * Purge all the messages in the queue
136     *
137     * @param queue - ObjectName of the queue to purge
138     * @throws Exception
139     */
140    public void purgeQueue(ObjectName queue) throws Exception {
141        context.printInfo("Purging all messages in queue: " + queue.getKeyProperty("destinationName"));
142        createJmxConnection().invoke(queue, "purge", new Object[] {}, new String[] {});
143        if (resetStatistics) {
144            createJmxConnection().invoke(queue, "resetStatistics", new Object[] {}, new String[] {});
145        }
146    }
147
148    /**
149     * Handle the --msgsel, --xmsgsel.
150     *
151     * @param token - option token to handle
152     * @param tokens - succeeding command arguments
153     * @throws Exception
154     */
155    @Override
156    protected void handleOption(String token, List<String> tokens) throws Exception {
157        // If token is an additive message selector option
158        if (token.startsWith("--msgsel")) {
159
160            // If no message selector is specified, or next token is a new
161            // option
162            if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
163                context.printException(new IllegalArgumentException("Message selector not specified"));
164                return;
165            }
166
167            StringTokenizer queryTokens = new StringTokenizer(tokens.remove(0), COMMAND_OPTION_DELIMETER);
168            while (queryTokens.hasMoreTokens()) {
169                queryAddObjects.add(queryTokens.nextToken());
170            }
171        } else if (token.startsWith("--xmsgsel")) {
172            // If token is a substractive message selector option
173
174            // If no message selector is specified, or next token is a new
175            // option
176            if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
177                context.printException(new IllegalArgumentException("Message selector not specified"));
178                return;
179            }
180
181            StringTokenizer queryTokens = new StringTokenizer(tokens.remove(0), COMMAND_OPTION_DELIMETER);
182            while (queryTokens.hasMoreTokens()) {
183                querySubObjects.add(queryTokens.nextToken());
184            }
185        } else if (token.startsWith("--reset")) {
186            resetStatistics = true;
187        } else {
188            // Let super class handle unknown option
189            super.handleOption(token, tokens);
190        }
191    }
192
193    /**
194     * Converts the message selector as provided on command line
195     * argument to activem-admin into an SQL-92 conform string.
196     * E.g.
197     *   "JMSMessageID='*:10',JMSPriority>5"
198     * gets converted into
199     *   "(JMSMessageID='%:10') AND (JMSPriority>5)"
200     *
201     * @param tokens - List of message selector query parameters
202     * @return SQL-92 string of that query.
203     */
204    public String convertToSQL92(List<String> tokens) {
205        StringBuilder selector = new StringBuilder();
206
207        boolean isFirstToken = true;
208        for (Iterator i = tokens.iterator(); i.hasNext(); ) {
209            String token = i.next().toString();
210            if (token.matches("^[^=]*='.*[\\*\\?].*'$")) {
211                token = token.replace('?', '_')
212                        .replace('*', '%')
213                        .replaceFirst("=", " LIKE ");
214            }
215            if (isFirstToken) {
216                isFirstToken = false;
217            } else {
218                selector.append(" AND ");
219            }
220            selector.append('(')
221                    .append(token)
222                    .append(')');
223        }
224        return selector.toString();
225    }
226
227    /**
228     * Print the help messages for the browse command
229     */
230    @Override
231    protected void printHelp() {
232        context.printHelp(helpFile);
233    }
234
235}