1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server;
18
19import android.util.Slog;
20import com.google.android.collect.Lists;
21
22import java.util.ArrayList;
23
24/**
25 * Parsed event from native side of {@link NativeDaemonConnector}.
26 */
27public class NativeDaemonEvent {
28
29    // TODO: keep class ranges in sync with ResponseCode.h
30    // TODO: swap client and server error ranges to roughly mirror HTTP spec
31
32    private final int mCmdNumber;
33    private final int mCode;
34    private final String mMessage;
35    private final String mRawEvent;
36    private String[] mParsed;
37
38    private NativeDaemonEvent(int cmdNumber, int code, String message, String rawEvent) {
39        mCmdNumber = cmdNumber;
40        mCode = code;
41        mMessage = message;
42        mRawEvent = rawEvent;
43        mParsed = null;
44    }
45
46    public int getCmdNumber() {
47        return mCmdNumber;
48    }
49
50    public int getCode() {
51        return mCode;
52    }
53
54    public String getMessage() {
55        return mMessage;
56    }
57
58    @Deprecated
59    public String getRawEvent() {
60        return mRawEvent;
61    }
62
63    @Override
64    public String toString() {
65        return mRawEvent;
66    }
67
68    /**
69     * Test if event represents a partial response which is continued in
70     * additional subsequent events.
71     */
72    public boolean isClassContinue() {
73        return mCode >= 100 && mCode < 200;
74    }
75
76    /**
77     * Test if event represents a command success.
78     */
79    public boolean isClassOk() {
80        return mCode >= 200 && mCode < 300;
81    }
82
83    /**
84     * Test if event represents a remote native daemon error.
85     */
86    public boolean isClassServerError() {
87        return mCode >= 400 && mCode < 500;
88    }
89
90    /**
91     * Test if event represents a command syntax or argument error.
92     */
93    public boolean isClassClientError() {
94        return mCode >= 500 && mCode < 600;
95    }
96
97    /**
98     * Test if event represents an unsolicited event from native daemon.
99     */
100    public boolean isClassUnsolicited() {
101        return isClassUnsolicited(mCode);
102    }
103
104    private static boolean isClassUnsolicited(int code) {
105        return code >= 600 && code < 700;
106    }
107
108    /**
109     * Verify this event matches the given code.
110     *
111     * @throws IllegalStateException if {@link #getCode()} doesn't match.
112     */
113    public void checkCode(int code) {
114        if (mCode != code) {
115            throw new IllegalStateException("Expected " + code + " but was: " + this);
116        }
117    }
118
119    /**
120     * Parse the given raw event into {@link NativeDaemonEvent} instance.
121     *
122     * @throws IllegalArgumentException when line doesn't match format expected
123     *             from native side.
124     */
125    public static NativeDaemonEvent parseRawEvent(String rawEvent) {
126        final String[] parsed = rawEvent.split(" ");
127        if (parsed.length < 2) {
128            throw new IllegalArgumentException("Insufficient arguments");
129        }
130
131        int skiplength = 0;
132
133        final int code;
134        try {
135            code = Integer.parseInt(parsed[0]);
136            skiplength = parsed[0].length() + 1;
137        } catch (NumberFormatException e) {
138            throw new IllegalArgumentException("problem parsing code", e);
139        }
140
141        int cmdNumber = -1;
142        if (isClassUnsolicited(code) == false) {
143            if (parsed.length < 3) {
144                throw new IllegalArgumentException("Insufficient arguemnts");
145            }
146            try {
147                cmdNumber = Integer.parseInt(parsed[1]);
148                skiplength += parsed[1].length() + 1;
149            } catch (NumberFormatException e) {
150                throw new IllegalArgumentException("problem parsing cmdNumber", e);
151            }
152        }
153
154        final String message = rawEvent.substring(skiplength);
155
156        return new NativeDaemonEvent(cmdNumber, code, message, rawEvent);
157    }
158
159    /**
160     * Filter the given {@link NativeDaemonEvent} list, returning
161     * {@link #getMessage()} for any events matching the requested code.
162     */
163    public static String[] filterMessageList(NativeDaemonEvent[] events, int matchCode) {
164        final ArrayList<String> result = Lists.newArrayList();
165        for (NativeDaemonEvent event : events) {
166            if (event.getCode() == matchCode) {
167                result.add(event.getMessage());
168            }
169        }
170        return result.toArray(new String[result.size()]);
171    }
172
173    /**
174     * Find the Nth field of the event.
175     *
176     * This ignores and code or cmdNum, the first return value is given for N=0.
177     * Also understands "\"quoted\" multiword responses" and tries them as a single field
178     */
179    public String getField(int n) {
180        if (mParsed == null) {
181            mParsed = unescapeArgs(mRawEvent);
182        }
183        n += 2; // skip code and command#
184        if (n > mParsed.length) return null;
185            return mParsed[n];
186        }
187
188    public static String[] unescapeArgs(String rawEvent) {
189        final boolean DEBUG_ROUTINE = false;
190        final String LOGTAG = "unescapeArgs";
191        final ArrayList<String> parsed = new ArrayList<String>();
192        final int length = rawEvent.length();
193        int current = 0;
194        int wordEnd = -1;
195        boolean quoted = false;
196
197        if (DEBUG_ROUTINE) Slog.e(LOGTAG, "parsing '" + rawEvent + "'");
198        if (rawEvent.charAt(current) == '\"') {
199            quoted = true;
200            current++;
201        }
202        while (current < length) {
203            // find the end of the word
204            if (quoted) {
205                wordEnd = current;
206                while ((wordEnd = rawEvent.indexOf('\"', wordEnd)) != -1) {
207                    if (rawEvent.charAt(wordEnd - 1) != '\\') {
208                        break;
209                    } else {
210                        wordEnd++; // skip this escaped quote and keep looking
211                    }
212                }
213            } else {
214                wordEnd = rawEvent.indexOf(' ', current);
215            }
216            // if we didn't find the end-o-word token, take the rest of the string
217            if (wordEnd == -1) wordEnd = length;
218            String word = rawEvent.substring(current, wordEnd);
219            current += word.length();
220            if (!quoted) {
221                word = word.trim();
222            } else {
223                current++;  // skip the trailing quote
224            }
225            // unescape stuff within the word
226            word = word.replace("\\\\", "\\");
227            word = word.replace("\\\"", "\"");
228
229            if (DEBUG_ROUTINE) Slog.e(LOGTAG, "found '" + word + "'");
230            parsed.add(word);
231
232            // find the beginning of the next word - either of these options
233            int nextSpace = rawEvent.indexOf(' ', current);
234            int nextQuote = rawEvent.indexOf(" \"", current);
235            if (DEBUG_ROUTINE) {
236                Slog.e(LOGTAG, "nextSpace=" + nextSpace + ", nextQuote=" + nextQuote);
237            }
238            if (nextQuote > -1 && nextQuote <= nextSpace) {
239                quoted = true;
240                current = nextQuote + 2;
241            } else {
242                quoted = false;
243                if (nextSpace > -1) {
244                    current = nextSpace + 1;
245                }
246            } // else we just start the next word after the current and read til the end
247            if (DEBUG_ROUTINE) {
248                Slog.e(LOGTAG, "next loop - current=" + current +
249                        ", length=" + length + ", quoted=" + quoted);
250            }
251        }
252        return parsed.toArray(new String[parsed.size()]);
253    }
254}
255