1 | |
|
2 | |
|
3 | |
|
4 | |
|
5 | |
|
6 | |
|
7 | |
|
8 | |
|
9 | |
|
10 | |
|
11 | |
package org.mule.transport.http; |
12 | |
|
13 | |
import org.mule.DefaultMuleEvent; |
14 | |
import org.mule.DefaultMuleMessage; |
15 | |
import org.mule.DefaultMuleSession; |
16 | |
import org.mule.MuleServer; |
17 | |
import org.mule.NullSessionHandler; |
18 | |
import org.mule.OptimizedRequestContext; |
19 | |
import org.mule.RequestContext; |
20 | |
import org.mule.api.MessagingException; |
21 | |
import org.mule.api.MuleEvent; |
22 | |
import org.mule.api.MuleException; |
23 | |
import org.mule.api.MuleMessage; |
24 | |
import org.mule.api.config.MuleProperties; |
25 | |
import org.mule.api.endpoint.EndpointURI; |
26 | |
import org.mule.api.endpoint.ImmutableEndpoint; |
27 | |
import org.mule.api.endpoint.InboundEndpoint; |
28 | |
import org.mule.api.lifecycle.CreateException; |
29 | |
import org.mule.api.service.Service; |
30 | |
import org.mule.api.transformer.TransformerException; |
31 | |
import org.mule.api.transport.Connector; |
32 | |
import org.mule.api.transport.MessageAdapter; |
33 | |
import org.mule.api.transport.MessageReceiver; |
34 | |
import org.mule.transport.ConnectException; |
35 | |
import org.mule.transport.NullPayload; |
36 | |
import org.mule.transport.http.i18n.HttpMessages; |
37 | |
import org.mule.transport.tcp.TcpConnector; |
38 | |
import org.mule.transport.tcp.TcpMessageReceiver; |
39 | |
import org.mule.util.MapUtils; |
40 | |
import org.mule.util.ObjectUtils; |
41 | |
import org.mule.util.monitor.Expirable; |
42 | |
|
43 | |
import java.io.IOException; |
44 | |
import java.net.Socket; |
45 | |
import java.net.SocketAddress; |
46 | |
import java.util.HashMap; |
47 | |
import java.util.Iterator; |
48 | |
import java.util.Map; |
49 | |
|
50 | |
import javax.resource.spi.work.Work; |
51 | |
|
52 | |
import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit; |
53 | |
|
54 | |
import org.apache.commons.httpclient.Cookie; |
55 | |
import org.apache.commons.httpclient.Header; |
56 | |
import org.apache.commons.httpclient.cookie.MalformedCookieException; |
57 | |
import org.apache.commons.logging.Log; |
58 | |
import org.apache.commons.logging.LogFactory; |
59 | |
|
60 | |
|
61 | |
|
62 | |
|
63 | |
|
64 | 1975 | public class HttpMessageReceiver extends TcpMessageReceiver |
65 | |
{ |
66 | 130 | protected final Log logger = LogFactory.getLog(getClass()); |
67 | |
|
68 | |
public HttpMessageReceiver(Connector connector, Service service, InboundEndpoint endpoint) |
69 | |
throws CreateException |
70 | |
{ |
71 | 130 | super(connector, service, endpoint); |
72 | 130 | } |
73 | |
|
74 | |
|
75 | |
protected Work createWork(Socket socket) throws IOException |
76 | |
{ |
77 | 152 | return new HttpWorker(socket); |
78 | |
} |
79 | |
|
80 | |
|
81 | |
protected void doConnect() throws ConnectException |
82 | |
{ |
83 | |
|
84 | |
|
85 | 120 | if (this.shouldConnect()) |
86 | |
{ |
87 | 116 | super.doConnect(); |
88 | |
} |
89 | 120 | } |
90 | |
|
91 | |
protected boolean shouldConnect() |
92 | |
{ |
93 | 120 | StringBuffer requestUri = new StringBuffer(80); |
94 | 120 | requestUri.append(endpoint.getProtocol()).append("://"); |
95 | 120 | requestUri.append(endpoint.getEndpointURI().getHost()); |
96 | 120 | requestUri.append(':').append(endpoint.getEndpointURI().getPort()); |
97 | 120 | requestUri.append('*'); |
98 | |
|
99 | 120 | MessageReceiver[] receivers = connector.getReceivers(requestUri.toString()); |
100 | 239 | for (int i = 0; i < receivers.length; i++) |
101 | |
{ |
102 | 123 | if (receivers[i].isConnected()) |
103 | |
{ |
104 | 4 | return false; |
105 | |
} |
106 | |
} |
107 | |
|
108 | 116 | return true; |
109 | |
} |
110 | |
|
111 | |
|
112 | |
|
113 | |
protected MuleMessage handleUnacceptedFilter(MuleMessage message) |
114 | |
{ |
115 | 2 | if (logger.isDebugEnabled()) |
116 | |
{ |
117 | 0 | logger.debug("Message request '" + message.getProperty(HttpConnector.HTTP_REQUEST_PROPERTY) |
118 | |
+ "' is being rejected since it does not match the filter on this endpoint: " + endpoint); |
119 | |
} |
120 | 2 | message.setProperty(HttpConnector.HTTP_STATUS_PROPERTY, String.valueOf(HttpConstants.SC_NOT_ACCEPTABLE)); |
121 | 2 | return message; |
122 | |
} |
123 | |
|
124 | |
protected class HttpWorker implements Work, Expirable |
125 | |
{ |
126 | |
private HttpServerConnection conn; |
127 | |
private String cookieSpec; |
128 | |
private boolean enableCookies; |
129 | |
private String remoteClientAddress; |
130 | |
|
131 | |
public HttpWorker(Socket socket) throws IOException |
132 | 152 | { |
133 | 152 | String encoding = endpoint.getEncoding(); |
134 | 152 | if (encoding == null) |
135 | |
{ |
136 | 0 | encoding = MuleServer.getMuleContext().getConfiguration().getDefaultEncoding(); |
137 | |
} |
138 | |
|
139 | 152 | conn = new HttpServerConnection(socket, encoding, (HttpConnector) connector); |
140 | |
|
141 | 152 | cookieSpec = |
142 | |
MapUtils.getString(endpoint.getProperties(), HttpConnector.HTTP_COOKIE_SPEC_PROPERTY, |
143 | |
((HttpConnector) connector).getCookieSpec()); |
144 | 152 | enableCookies = |
145 | |
MapUtils.getBooleanValue(endpoint.getProperties(), HttpConnector.HTTP_ENABLE_COOKIES_PROPERTY, |
146 | |
((HttpConnector) connector).isEnableCookies()); |
147 | |
|
148 | 152 | final SocketAddress clientAddress = socket.getRemoteSocketAddress(); |
149 | 152 | if (clientAddress != null) |
150 | |
{ |
151 | 152 | remoteClientAddress = clientAddress.toString(); |
152 | |
} |
153 | 152 | } |
154 | |
|
155 | |
public void expired() |
156 | |
{ |
157 | 0 | if (conn.isOpen()) |
158 | |
{ |
159 | 0 | conn.close(); |
160 | |
} |
161 | 0 | } |
162 | |
|
163 | |
public void run() |
164 | |
{ |
165 | 152 | long keepAliveTimeout = ((TcpConnector) connector).getKeepAliveTimeout(); |
166 | |
|
167 | |
try |
168 | |
{ |
169 | |
do |
170 | |
{ |
171 | 158 | conn.setKeepAlive(false); |
172 | |
|
173 | |
|
174 | 158 | if(keepAliveTimeout > 0) |
175 | |
{ |
176 | 0 | ((HttpConnector) connector).getKeepAliveMonitor().addExpirable( |
177 | |
keepAliveTimeout, TimeUnit.MILLISECONDS, this); |
178 | |
} |
179 | |
|
180 | 158 | HttpRequest request = conn.readRequest(); |
181 | 155 | if (request == null) |
182 | |
{ |
183 | 3 | break; |
184 | |
} |
185 | |
|
186 | |
|
187 | 152 | ((HttpConnector) connector).getKeepAliveMonitor().removeExpirable(this); |
188 | |
|
189 | 152 | conn.writeResponse(processRequest(request)); |
190 | |
} |
191 | 152 | while (conn.isKeepAlive()); |
192 | |
} |
193 | 3 | catch (Exception e) |
194 | |
{ |
195 | 3 | handleException(e); |
196 | |
} |
197 | |
finally |
198 | |
{ |
199 | 152 | logger.info("Closing HTTP connection."); |
200 | |
|
201 | 152 | if (conn.isOpen()) |
202 | |
{ |
203 | 149 | conn.close(); |
204 | 149 | conn = null; |
205 | |
|
206 | |
|
207 | 149 | ((HttpConnector) connector).getKeepAliveMonitor().removeExpirable(this); |
208 | |
} |
209 | |
} |
210 | 150 | } |
211 | |
|
212 | |
protected HttpResponse processRequest(HttpRequest request) throws MuleException, IOException |
213 | |
{ |
214 | 152 | RequestLine requestLine = request.getRequestLine(); |
215 | 152 | String method = requestLine.getMethod(); |
216 | |
|
217 | 152 | if (method.equals(HttpConstants.METHOD_HEAD)) |
218 | |
{ |
219 | 2 | return doHead(requestLine); |
220 | |
} |
221 | 150 | else if (method.equals(HttpConstants.METHOD_GET) |
222 | |
|| method.equals(HttpConstants.METHOD_POST) |
223 | |
|| method.equals(HttpConstants.METHOD_OPTIONS) |
224 | |
|| method.equals(HttpConstants.METHOD_PUT) |
225 | |
|| method.equals(HttpConstants.METHOD_DELETE) |
226 | |
|| method.equals(HttpConstants.METHOD_TRACE) |
227 | |
|| method.equals(HttpConstants.METHOD_CONNECT)) |
228 | |
{ |
229 | 148 | return doRequest(request, requestLine); |
230 | |
} |
231 | |
else |
232 | |
{ |
233 | 2 | return doBad(requestLine); |
234 | |
} |
235 | |
} |
236 | |
|
237 | |
protected HttpResponse doHead(RequestLine requestLine) throws MuleException |
238 | |
{ |
239 | 2 | MuleMessage message = new DefaultMuleMessage(NullPayload.getInstance()); |
240 | 2 | MuleEvent event = new DefaultMuleEvent(message, endpoint, new DefaultMuleSession(message, new NullSessionHandler(), connector.getMuleContext()), true); |
241 | 2 | OptimizedRequestContext.unsafeSetEvent(event); |
242 | 2 | HttpResponse response = new HttpResponse(); |
243 | 2 | response.setStatusLine(requestLine.getHttpVersion(), HttpConstants.SC_OK); |
244 | 2 | return transformResponse(response); |
245 | |
} |
246 | |
|
247 | |
protected HttpResponse doRequest(HttpRequest request, |
248 | |
RequestLine requestLine) throws IOException, MuleException |
249 | |
{ |
250 | 148 | Map headers = parseHeaders(request); |
251 | |
|
252 | |
|
253 | 148 | MessageAdapter adapter = buildStandardAdapter(request, headers); |
254 | |
|
255 | 148 | MuleMessage message = new DefaultMuleMessage(adapter); |
256 | |
|
257 | 148 | if (logger.isDebugEnabled()) |
258 | |
{ |
259 | 0 | logger.debug(message.getProperty(HttpConnector.HTTP_REQUEST_PROPERTY)); |
260 | |
} |
261 | |
|
262 | |
|
263 | 148 | MessageReceiver receiver = getTargetReceiver(message, endpoint); |
264 | |
|
265 | |
HttpResponse response; |
266 | |
|
267 | |
|
268 | 148 | if (receiver != null) |
269 | |
{ |
270 | 146 | preRouteMessage(message); |
271 | 146 | MuleMessage returnMessage = receiver.routeMessage(message, endpoint.isSynchronous(), null); |
272 | |
|
273 | |
Object tempResponse; |
274 | 146 | if (returnMessage != null) |
275 | |
{ |
276 | 138 | tempResponse = returnMessage.getPayload(); |
277 | |
} |
278 | |
else |
279 | |
{ |
280 | 8 | tempResponse = NullPayload.getInstance(); |
281 | |
} |
282 | |
|
283 | |
|
284 | 146 | if (tempResponse instanceof HttpResponse) |
285 | |
{ |
286 | 128 | response = (HttpResponse)tempResponse; |
287 | |
} |
288 | |
else |
289 | |
{ |
290 | 18 | response = transformResponse(returnMessage); |
291 | |
} |
292 | 146 | response.disableKeepAlive(!((HttpConnector)connector).isKeepAlive()); |
293 | 146 | } |
294 | |
else |
295 | |
{ |
296 | 2 | response = buildFailureResponse(requestLine, message); |
297 | |
} |
298 | 148 | return response; |
299 | |
} |
300 | |
|
301 | |
protected HttpResponse doOtherValid(RequestLine requestLine, String method) throws MuleException |
302 | |
{ |
303 | 0 | MuleMessage message = new DefaultMuleMessage(NullPayload.getInstance()); |
304 | 0 | MuleEvent event = new DefaultMuleEvent(message, endpoint, new DefaultMuleSession(message, new NullSessionHandler(), connector.getMuleContext()), true); |
305 | 0 | OptimizedRequestContext.unsafeSetEvent(event); |
306 | 0 | HttpResponse response = new HttpResponse(); |
307 | 0 | response.setStatusLine(requestLine.getHttpVersion(), HttpConstants.SC_METHOD_NOT_ALLOWED); |
308 | 0 | response.setBody(HttpMessages.methodNotAllowed(method).toString() + HttpConstants.CRLF); |
309 | 0 | return transformResponse(response); |
310 | |
} |
311 | |
|
312 | |
protected HttpResponse doBad(RequestLine requestLine) throws MuleException |
313 | |
{ |
314 | 2 | MuleMessage message = new DefaultMuleMessage(NullPayload.getInstance()); |
315 | 2 | MuleEvent event = new DefaultMuleEvent(message, endpoint, new DefaultMuleSession(message, new NullSessionHandler(), connector.getMuleContext()), true); |
316 | 2 | OptimizedRequestContext.unsafeSetEvent(event); |
317 | 2 | HttpResponse response = new HttpResponse(); |
318 | 2 | response.setStatusLine(requestLine.getHttpVersion(), HttpConstants.SC_BAD_REQUEST); |
319 | 2 | response.setBody(HttpMessages.malformedSyntax().toString() + HttpConstants.CRLF); |
320 | 2 | return transformResponse(response); |
321 | |
} |
322 | |
|
323 | |
protected MessageAdapter buildStandardAdapter(final HttpRequest request, |
324 | |
final Map headers) throws MessagingException, TransformerException, IOException |
325 | |
{ |
326 | 148 | final RequestLine requestLine = request.getRequestLine(); |
327 | |
|
328 | 148 | sendExpect100(headers, requestLine); |
329 | |
|
330 | 148 | Object body = request.getBody(); |
331 | 148 | if (body == null) |
332 | |
{ |
333 | 22 | body = requestLine.getUri(); |
334 | |
} |
335 | |
|
336 | 148 | return connector.getMessageAdapter(new Object[]{body, headers}); |
337 | |
} |
338 | |
|
339 | |
private void sendExpect100(Map headers, RequestLine requestLine) |
340 | |
throws TransformerException, IOException |
341 | |
{ |
342 | |
|
343 | |
|
344 | |
|
345 | |
|
346 | 148 | if (HttpConstants.HTTP11.equals(headers.get(HttpConnector.HTTP_VERSION_PROPERTY))) |
347 | |
{ |
348 | |
|
349 | |
|
350 | 148 | String expectHeaderValue = ObjectUtils.toString( |
351 | |
headers.get(HttpConstants.HEADER_EXPECT)).toLowerCase(); |
352 | 148 | if (HttpConstants.HEADER_EXPECT_CONTINUE_REQUEST_VALUE.equals(expectHeaderValue)) |
353 | |
{ |
354 | 2 | HttpResponse expected = new HttpResponse(); |
355 | 2 | expected.setStatusLine(requestLine.getHttpVersion(), HttpConstants.SC_CONTINUE); |
356 | 2 | final DefaultMuleEvent event = new DefaultMuleEvent(new DefaultMuleMessage(expected), endpoint, |
357 | |
new DefaultMuleSession(service, connector.getMuleContext()), true); |
358 | 2 | RequestContext.setEvent(event); |
359 | 2 | conn.writeResponse(transformResponse(expected)); |
360 | |
} |
361 | |
} |
362 | 148 | } |
363 | |
|
364 | |
protected HttpResponse buildFailureResponse(RequestLine requestLine, MuleMessage message) throws TransformerException |
365 | |
{ |
366 | 2 | EndpointURI uri = endpoint.getEndpointURI(); |
367 | 2 | String failedPath = uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() |
368 | |
+ getRequestPath(message); |
369 | |
|
370 | 2 | if (logger.isDebugEnabled()) |
371 | |
{ |
372 | 0 | logger.debug("Failed to bind to " + failedPath); |
373 | |
} |
374 | |
|
375 | 2 | HttpResponse response = new HttpResponse(); |
376 | 2 | response.setStatusLine(requestLine.getHttpVersion(), HttpConstants.SC_NOT_FOUND); |
377 | 2 | response.setBody(HttpMessages.cannotBindToAddress(failedPath).toString()); |
378 | 2 | RequestContext.setEvent(new DefaultMuleEvent(new DefaultMuleMessage(response), endpoint, |
379 | |
new DefaultMuleSession(service, connector.getMuleContext()), true)); |
380 | |
|
381 | 2 | return transformResponse(response); |
382 | |
} |
383 | |
|
384 | |
protected Map parseHeaders(HttpRequest request) throws MalformedCookieException |
385 | |
{ |
386 | 148 | RequestLine requestLine = request.getRequestLine(); |
387 | 148 | Map headers = new HashMap(); |
388 | |
|
389 | 148 | for (Iterator rhi = request.getHeaderIterator(); rhi.hasNext();) |
390 | |
{ |
391 | 836 | Header header = (Header)rhi.next(); |
392 | 836 | String headerName = header.getName(); |
393 | 836 | Object headerValue = header.getValue(); |
394 | |
|
395 | |
|
396 | 836 | if (headerName.startsWith("X-MULE")) |
397 | |
{ |
398 | 260 | headerName = headerName.substring(2); |
399 | |
} |
400 | |
|
401 | 576 | else if (headerName.equals(HttpConnector.HTTP_COOKIES_PROPERTY)) |
402 | |
{ |
403 | 0 | if (enableCookies) |
404 | |
{ |
405 | 0 | Cookie[] cookies = CookieHelper.parseCookies(header, cookieSpec); |
406 | 0 | if (cookies.length > 0) |
407 | |
{ |
408 | |
|
409 | 0 | headerValue = cookies; |
410 | |
} |
411 | |
else |
412 | |
{ |
413 | |
|
414 | |
continue; |
415 | |
} |
416 | |
} |
417 | |
else |
418 | |
{ |
419 | |
|
420 | |
continue; |
421 | |
} |
422 | |
} |
423 | |
|
424 | |
|
425 | 836 | headers.put(headerName, headerValue); |
426 | 836 | } |
427 | |
|
428 | 148 | headers.put(HttpConnector.HTTP_METHOD_PROPERTY, requestLine.getMethod()); |
429 | 148 | headers.put(HttpConnector.HTTP_REQUEST_PROPERTY, requestLine.getUri()); |
430 | 148 | headers.put(HttpConnector.HTTP_VERSION_PROPERTY, requestLine.getHttpVersion().toString()); |
431 | 148 | headers.put(HttpConnector.HTTP_COOKIE_SPEC_PROPERTY, cookieSpec); |
432 | 148 | return headers; |
433 | |
} |
434 | |
|
435 | |
protected void preRouteMessage(MuleMessage message) |
436 | |
{ |
437 | 146 | message.setProperty(MuleProperties.MULE_REMOTE_CLIENT_ADDRESS, remoteClientAddress); |
438 | 146 | } |
439 | |
|
440 | |
public void release() |
441 | |
{ |
442 | 0 | conn.close(); |
443 | 0 | conn = null; |
444 | 0 | } |
445 | |
} |
446 | |
|
447 | |
protected String getRequestPath(MuleMessage message) |
448 | |
{ |
449 | 2 | String path = (String) message.getProperty(HttpConnector.HTTP_REQUEST_PROPERTY); |
450 | 2 | int i = path.indexOf('?'); |
451 | 2 | if (i > -1) |
452 | |
{ |
453 | 0 | path = path.substring(0, i); |
454 | |
} |
455 | 2 | return path; |
456 | |
} |
457 | |
|
458 | |
protected MessageReceiver getTargetReceiver(MuleMessage message, ImmutableEndpoint endpoint) |
459 | |
throws ConnectException |
460 | |
{ |
461 | 148 | String path = (String) message.getProperty(HttpConnector.HTTP_REQUEST_PROPERTY); |
462 | 148 | int i = path.indexOf('?'); |
463 | 148 | if (i > -1) |
464 | |
{ |
465 | 4 | path = path.substring(0, i); |
466 | |
} |
467 | |
|
468 | 148 | StringBuffer requestUri = new StringBuffer(80); |
469 | 148 | if (path.indexOf("://")==-1) |
470 | |
{ |
471 | 148 | requestUri.append(endpoint.getProtocol()).append("://"); |
472 | 148 | requestUri.append(endpoint.getEndpointURI().getHost()); |
473 | 148 | requestUri.append(':').append(endpoint.getEndpointURI().getPort()); |
474 | |
} |
475 | |
|
476 | 148 | if (logger.isTraceEnabled()) |
477 | |
{ |
478 | 0 | logger.trace("Looking up receiver on connector: " + connector.getName() + " with URI key: " |
479 | |
+ requestUri.toString()); |
480 | |
} |
481 | |
|
482 | 148 | MessageReceiver receiver = connector.lookupReceiver(requestUri.toString()); |
483 | |
|
484 | |
|
485 | |
|
486 | 148 | if (receiver == null && !"/".equals(path)) |
487 | |
{ |
488 | |
|
489 | 94 | int x = path.lastIndexOf('/'); |
490 | 94 | if (x > 1 && path.indexOf('.') > x) |
491 | |
{ |
492 | 0 | requestUri.append(path.substring(0, x)); |
493 | |
} |
494 | |
else |
495 | |
{ |
496 | 94 | requestUri.append(path); |
497 | |
} |
498 | |
|
499 | 94 | if (logger.isDebugEnabled()) |
500 | |
{ |
501 | 0 | logger.debug("Secondary lookup of receiver on connector: " + connector.getName() |
502 | |
+ " with URI key: " + requestUri.toString()); |
503 | |
} |
504 | |
|
505 | |
|
506 | 94 | String uriStr = requestUri.toString(); |
507 | 94 | receiver = connector.lookupReceiver(uriStr); |
508 | |
|
509 | 94 | if (receiver == null) |
510 | |
{ |
511 | 10 | receiver = findReceiverByStem(connector.getReceivers(), uriStr); |
512 | |
} |
513 | |
|
514 | 94 | if (receiver == null && logger.isWarnEnabled()) |
515 | |
{ |
516 | 2 | logger.warn("No receiver found with secondary lookup on connector: " + connector.getName() |
517 | |
+ " with URI key: " + requestUri.toString()); |
518 | 2 | logger.warn("Receivers on connector are: " |
519 | |
+ MapUtils.toString(connector.getReceivers(), true)); |
520 | |
} |
521 | |
} |
522 | |
|
523 | 148 | return receiver; |
524 | |
} |
525 | |
|
526 | |
protected HttpResponse transformResponse(Object response) throws TransformerException |
527 | |
{ |
528 | |
MuleMessage message; |
529 | 26 | if(response instanceof MuleMessage) |
530 | |
{ |
531 | 10 | message = (MuleMessage)response; |
532 | |
} |
533 | |
else |
534 | |
{ |
535 | 16 | message = new DefaultMuleMessage(response); |
536 | |
} |
537 | |
|
538 | |
|
539 | 26 | message.applyTransformers(connector.getDefaultResponseTransformers(), HttpResponse.class); |
540 | 26 | return (HttpResponse) message.getPayload(); |
541 | |
} |
542 | |
|
543 | |
public static MessageReceiver findReceiverByStem(Map receivers, String uriStr) |
544 | |
{ |
545 | 10 | int match = 0; |
546 | 10 | MessageReceiver receiver = null; |
547 | 10 | for (Iterator itr = receivers.entrySet().iterator(); itr.hasNext();) |
548 | |
{ |
549 | 16 | Map.Entry e = (Map.Entry)itr.next(); |
550 | 16 | String key = (String)e.getKey(); |
551 | 16 | MessageReceiver candidate = (MessageReceiver)e.getValue(); |
552 | 16 | if (uriStr.startsWith(key) && match < key.length()) |
553 | |
{ |
554 | 8 | match = key.length(); |
555 | 8 | receiver = candidate; |
556 | |
} |
557 | 16 | } |
558 | 10 | return receiver; |
559 | |
} |
560 | |
|
561 | |
} |