View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
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    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host;
16  
17  import static org.htmlunit.BrowserVersionFeatures.JS_LOCATION_IGNORE_QUERY_FOR_ABOUT_PROTOCOL;
18  import static org.htmlunit.BrowserVersionFeatures.JS_LOCATION_RELOAD_REFERRER;
19  
20  import java.io.IOException;
21  import java.lang.reflect.Method;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.htmlunit.BrowserVersion;
28  import org.htmlunit.Page;
29  import org.htmlunit.WebRequest;
30  import org.htmlunit.WebWindow;
31  import org.htmlunit.corejs.javascript.Context;
32  import org.htmlunit.corejs.javascript.Function;
33  import org.htmlunit.corejs.javascript.FunctionObject;
34  import org.htmlunit.corejs.javascript.ScriptableObject;
35  import org.htmlunit.corejs.javascript.VarScope;
36  import org.htmlunit.html.HtmlPage;
37  import org.htmlunit.javascript.HtmlUnitScriptable;
38  import org.htmlunit.javascript.JavaScriptEngine;
39  import org.htmlunit.javascript.configuration.JsxClass;
40  import org.htmlunit.javascript.configuration.JsxConstructor;
41  import org.htmlunit.javascript.host.event.Event;
42  import org.htmlunit.javascript.host.event.HashChangeEvent;
43  import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
44  import org.htmlunit.util.StringUtils;
45  import org.htmlunit.util.UrlUtils;
46  
47  /**
48   * JavaScript host object for {@code Location}.
49   *
50   * @author Mike Bowler
51   * @author Michael Ottati
52   * @author Marc Guillemot
53   * @author Chris Erskine
54   * @author Daniel Gredler
55   * @author David K. Taylor
56   * @author Ahmed Ashour
57   * @author Ronald Brill
58   * @author Frank Danek
59   * @author Adam Afeltowicz
60   * @author Atsushi Nakagawa
61   * @author Lai Quang Duong
62   * @author Kanoko Yamamoto
63   *
64   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location">MDN Documentation</a>
65   */
66  @JsxClass
67  public class Location extends HtmlUnitScriptable {
68  
69      private static final Log LOG = LogFactory.getLog(Location.class);
70      private static final String UNKNOWN = "null";
71  
72      /**
73       * The window which owns this location object.
74       */
75      private Window window_;
76  
77      private static final Method METHOD_ASSIGN;
78      private static final Method METHOD_RELOAD;
79      private static final Method METHOD_REPLACE;
80      private static final Method METHOD_TO_STRING;
81  
82      private static final Method GETTER_HASH;
83      private static final Method SETTER_HASH;
84  
85      private static final Method GETTER_HOST;
86      private static final Method SETTER_HOST;
87  
88      private static final Method GETTER_HOSTNAME;
89      private static final Method SETTER_HOSTNAME;
90  
91      private static final Method GETTER_HREF;
92      private static final Method SETTER_HREF;
93  
94      private static final Method GETTER_ORIGIN;
95  
96      private static final Method GETTER_PATHNAME;
97      private static final Method SETTER_PATHNAME;
98  
99      private static final Method GETTER_PORT;
100     private static final Method SETTER_PORT;
101 
102     private static final Method GETTER_PROTOCOL;
103     private static final Method SETTER_PROTOCOL;
104 
105     private static final Method GETTER_SEARCH;
106     private static final Method SETTER_SEARCH;
107 
108     static {
109         try {
110             METHOD_ASSIGN = Location.class.getDeclaredMethod("assign", String.class);
111             METHOD_RELOAD = Location.class.getDeclaredMethod("reload", boolean.class);
112             METHOD_REPLACE = Location.class.getDeclaredMethod("replace", String.class);
113             METHOD_TO_STRING = Location.class.getDeclaredMethod("jsToString");
114 
115             GETTER_HASH = Location.class.getDeclaredMethod("getHash");
116             SETTER_HASH = Location.class.getDeclaredMethod("setHash", String.class);
117 
118             GETTER_HOST = Location.class.getDeclaredMethod("getHost");
119             SETTER_HOST = Location.class.getDeclaredMethod("setHost", String.class);
120 
121             GETTER_HOSTNAME = Location.class.getDeclaredMethod("getHostname");
122             SETTER_HOSTNAME = Location.class.getDeclaredMethod("setHostname", String.class);
123 
124             GETTER_HREF = Location.class.getDeclaredMethod("getHref");
125             SETTER_HREF = Location.class.getDeclaredMethod("setHref", String.class);
126 
127             GETTER_ORIGIN = Location.class.getDeclaredMethod("getOrigin");
128 
129             GETTER_PATHNAME = Location.class.getDeclaredMethod("getPathname");
130             SETTER_PATHNAME = Location.class.getDeclaredMethod("setPathname", String.class);
131 
132             GETTER_PORT = Location.class.getDeclaredMethod("getPort");
133             SETTER_PORT = Location.class.getDeclaredMethod("setPort", String.class);
134 
135             GETTER_PROTOCOL = Location.class.getDeclaredMethod("getProtocol");
136             SETTER_PROTOCOL = Location.class.getDeclaredMethod("setProtocol", String.class);
137 
138             GETTER_SEARCH = Location.class.getDeclaredMethod("getSearch");
139             SETTER_SEARCH = Location.class.getDeclaredMethod("setSearch", String.class);
140         }
141         catch (NoSuchMethodException | SecurityException e) {
142             throw new RuntimeException(e);
143         }
144     }
145 
146     /**
147      * The current hash; cached locally to avoid modifying the page's URL and
148      * forcing a page reload each time this changes.
149      */
150     private String hash_;
151 
152     /**
153      * Creates an instance of this object.
154      *
155      * @param cx the current context
156      * @param scope the scope
157      * @param args the constructor arguments
158      * @param ctorObj the function object
159      * @param inNewExpr whether invoked via {@code new}
160      * @return the new {@code Location} instance
161      */
162     @JsxConstructor
163     public static Location jsConstructor(final Context cx, final VarScope scope,
164             final Object[] args, final Function ctorObj, final boolean inNewExpr) {
165 
166         final Location location = new Location();
167         location.setParentScope(scope);
168         location.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
169 
170         location.initialize(scope, null, null);
171 
172         return location;
173     }
174 
175     /**
176      * Initializes this {@code Location}.
177      *
178      * @param scope the scope
179      * @param window the window that this location belongs to
180      * @param page the page that will become the enclosing page
181      */
182     public void initialize(final VarScope scope, final Window window, final Page page) {
183         final int attributes = ScriptableObject.PERMANENT | ScriptableObject.READONLY;
184 
185         FunctionObject functionObject = new FunctionObject(METHOD_ASSIGN.getName(), METHOD_ASSIGN, scope);
186         defineProperty(METHOD_ASSIGN.getName(), functionObject, attributes);
187 
188         functionObject = new FunctionObject(METHOD_RELOAD.getName(), METHOD_RELOAD, scope);
189         defineProperty(METHOD_RELOAD.getName(), functionObject, attributes);
190 
191         functionObject = new FunctionObject(METHOD_REPLACE.getName(), METHOD_REPLACE, scope);
192         defineProperty(METHOD_REPLACE.getName(), functionObject, attributes);
193 
194         functionObject = new FunctionObject(METHOD_TO_STRING.getName(), METHOD_TO_STRING, scope);
195         defineProperty("toString", functionObject, attributes);
196 
197         defineProperty(scope, "hash", null, GETTER_HASH, SETTER_HASH, attributes);
198         defineProperty(scope, "host", null, GETTER_HOST, SETTER_HOST, attributes);
199         defineProperty(scope, "hostname", null, GETTER_HOSTNAME, SETTER_HOSTNAME, attributes);
200         defineProperty(scope, "href", null, GETTER_HREF, SETTER_HREF, attributes);
201         defineProperty(scope, "origin", null, GETTER_ORIGIN, null, attributes);
202         defineProperty(scope, "pathname", null, GETTER_PATHNAME, SETTER_PATHNAME, attributes);
203         defineProperty(scope, "port", null, GETTER_PORT, SETTER_PORT, attributes);
204         defineProperty(scope, "protocol", null, GETTER_PROTOCOL, SETTER_PROTOCOL, attributes);
205         defineProperty(scope, "search", null, GETTER_SEARCH, SETTER_SEARCH, attributes);
206 
207         window_ = window;
208         if (window_ != null && page != null) {
209             setHash(null, page.getUrl().getRef());
210         }
211     }
212 
213     /**
214      * {@inheritDoc}
215      */
216     @Override
217     public Object getDefaultValue(final Class<?> hint) {
218         if (getPrototype() != null
219                 && window_ != null
220                 && (hint == null || String.class.equals(hint))) {
221             return getHref();
222         }
223         return super.getDefaultValue(hint);
224     }
225 
226     /**
227      * Loads the new HTML document corresponding to the specified URL.
228      *
229      * @param url the URL of the new HTML document to load
230      * @throws IOException if loading the specified location fails
231      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/assign">MDN Documentation</a>
232      */
233     public void assign(final String url) throws IOException {
234         setHref(url);
235     }
236 
237     /**
238      * Reloads the current page, optionally forcing retrieval from the server even if
239      * the browser cache contains the latest version of the document.
240      *
241      * @param force if {@code true}, force reload from the server; otherwise, the browser may reload from cache
242      * @throws IOException if there is a problem reloading the page
243      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/reload">MDN Documentation</a>
244      */
245     public void reload(final boolean force) throws IOException {
246         final WebWindow webWindow = window_.getWebWindow();
247         final HtmlPage htmlPage = (HtmlPage) webWindow.getEnclosedPage();
248         final WebRequest request = htmlPage.getWebResponse().getWebRequest();
249 
250         // update request url with location.href in case hash was changed
251         request.setUrl(new URL(getHref()));
252         if (getBrowserVersion().hasFeature(JS_LOCATION_RELOAD_REFERRER)) {
253             request.setRefererHeader(htmlPage.getUrl());
254         }
255 
256         webWindow.getWebClient().download(webWindow, "", request, false, null, "JS location.reload");
257     }
258 
259     /**
260      * Loads the document at the specified URL, replacing the current entry in the session history
261      * so that the previous page cannot be reached via the back button.
262      *
263      * @param url the new URL to load
264      * @throws IOException if loading the specified location fails
265      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/replace">MDN Documentation</a>
266      */
267     public void replace(final String url) throws IOException {
268         window_.getWebWindow().getHistory().removeCurrent();
269         setHref(url);
270     }
271 
272     /**
273      * Returns the location URL as a string.
274      *
275      * @return the location URL
276      */
277     public String jsToString() {
278         if (window_ != null) {
279             return getHref();
280         }
281         return "";
282     }
283 
284     /**
285      * Returns the full URL of the current page.
286      *
287      * @return the location URL
288      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/href">MDN Documentation</a>
289      */
290     public String getHref() {
291         final WebWindow webWindow = window_.getWebWindow();
292         final Page page = webWindow.getEnclosedPage();
293         if (page == null) {
294             return UNKNOWN;
295         }
296         try {
297             URL url = page.getUrl();
298             final String hash = getHash(true);
299             if (hash != null) {
300                 url = UrlUtils.getUrlWithNewRef(url, hash);
301             }
302             String s = url.toExternalForm();
303             if (s.startsWith("file:/") && !s.startsWith("file:///")) {
304                 // Java (sometimes?) returns file URLs with a single slash; however, browsers return
305                 // three slashes. See http://www.cyanwerks.com/file-url-formats.html for more info.
306                 s = "file:///" + s.substring("file:/".length());
307             }
308             return s;
309         }
310         catch (final MalformedURLException e) {
311             if (LOG.isErrorEnabled()) {
312                 LOG.error(e.getMessage(), e);
313             }
314             return page.getUrl().toExternalForm();
315         }
316     }
317 
318     /**
319      * Sets the location URL to an entirely new value, navigating to the new page.
320      *
321      * @param newLocation the new location URL
322      * @throws IOException if loading the specified location fails
323      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/href">MDN Documentation</a>
324      */
325     public void setHref(final String newLocation) throws IOException {
326         if (newLocation.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
327             final String script = newLocation.substring(11);
328             final HtmlPage page = (HtmlPage) window_.getWebWindow().getEnclosedPage();
329             page.executeJavaScript(script, "new location value", 1);
330             return;
331         }
332 
333         try {
334             // we need to get the caller window to get the calling page
335             WebWindow webWindow = ((Window) JavaScriptEngine.getTopCallScope().getGlobalThis()).getWebWindow();
336             final HtmlPage callingPage = (HtmlPage) webWindow.getEnclosedPage();
337 
338             final BrowserVersion browserVersion = webWindow.getWebClient().getBrowserVersion();
339 
340             URL url = callingPage.getFullyQualifiedUrl(newLocation);
341             // fix for empty url
342             if (StringUtils.isEmptyOrNull(newLocation)) {
343                 url = UrlUtils.getUrlWithNewRef(url, null);
344             }
345 
346             final WebRequest request = new WebRequest(url,
347                         browserVersion.getHtmlAcceptHeader(), browserVersion.getAcceptEncodingHeader());
348             request.setRefererHeader(callingPage.getUrl());
349 
350             webWindow = window_.getWebWindow();
351             webWindow.getWebClient().download(webWindow, "", request, true, null, "JS set location");
352         }
353         catch (final MalformedURLException e) {
354             if (LOG.isErrorEnabled()) {
355                 LOG.error("setHref('" + newLocation + "') got MalformedURLException", e);
356             }
357             throw e;
358         }
359     }
360 
361     /**
362      * Returns the search portion of the location URL (the portion following the {@code ?}).
363      *
364      * @return the search portion of the location URL
365      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/search">MDN Documentation</a>
366      */
367     public String getSearch() {
368         final URL url = getUrl();
369         final String search = url.getQuery();
370         if (StringUtils.isEmptyOrNull(search)) {
371             return "";
372         }
373 
374         if (StringUtils.startsWithIgnoreCase(url.getProtocol(), UrlUtils.ABOUT)
375                 && window_.getWebWindow().getWebClient().getBrowserVersion()
376                                 .hasFeature(JS_LOCATION_IGNORE_QUERY_FOR_ABOUT_PROTOCOL)) {
377             return "";
378         }
379         return "?" + search;
380     }
381 
382     /**
383      * Sets the search portion of the location URL (the portion following the {@code ?}).
384      *
385      * @param search the new search portion of the location URL
386      * @throws Exception if an error occurs
387      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/search">MDN Documentation</a>
388      */
389     public void setSearch(final String search) throws Exception {
390         setUrl(UrlUtils.getUrlWithNewQuery(getUrl(), search));
391     }
392 
393     /**
394      * Returns the hash portion of the location URL (the portion following the {@code #}).
395      *
396      * @return the hash portion of the location URL
397      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hash">MDN Documentation</a>
398      */
399     public String getHash() {
400         if (StringUtils.isEmptyOrNull(hash_)) {
401             return "";
402         }
403 
404         if (hash_.indexOf('%') == -1) {
405             return "#" + UrlUtils.encodeHash(hash_);
406         }
407         return "#" + UrlUtils.encodeHash(UrlUtils.decode(hash_));
408     }
409 
410     private String getHash(final boolean encoded) {
411         if (hash_ == null || hash_.isEmpty()) {
412             return null;
413         }
414         if (encoded) {
415             return UrlUtils.encodeHash(hash_);
416         }
417         return hash_;
418     }
419 
420     /**
421      * Sets the hash portion of the location URL (the portion following the {@code #}).
422      * This method does not cause a server hit.
423      *
424      * @param hash the new hash portion of the location URL
425      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hash">MDN Documentation</a>
426      */
427     public void setHash(final String hash) {
428         // IMPORTANT: This method must not call setUrl(), because
429         // we must not hit the server just to change the hash!
430         setHash(getHref(), hash, true);
431     }
432 
433     /**
434      * Sets the hash portion of the location URL (the portion following the {@code #}).
435      *
436      * @param oldURL the old URL
437      * @param hash the new hash portion of the location URL
438      */
439     public void setHash(final String oldURL, final String hash) {
440         setHash(oldURL, hash, true);
441     }
442 
443     /**
444      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
445      *
446      * Sets the hash portion of the location URL (the portion following the {@code #}).
447      *
448      * @param oldURL the old URL
449      * @param hash the new hash portion of the location URL
450      * @param triggerHashChanged option to disable event triggering
451      */
452     public void setHash(final String oldURL, String hash, final boolean triggerHashChanged) {
453         // IMPORTANT: This method must not call setUrl(), because
454         // we must not hit the server just to change the hash!
455         if (hash != null && !hash.isEmpty() && hash.charAt(0) == '#') {
456             hash = hash.substring(1);
457         }
458         final boolean hasChanged = hash != null && !hash.equals(hash_);
459         hash_ = hash;
460 
461         if (hasChanged) {
462             final Window w = getWindow();
463             final Page page = w.getWebWindow().getEnclosedPage();
464             if (page != null) {
465                 final WebRequest request = page.getWebResponse().getWebRequest();
466                 request.setUrl(UrlUtils.toUrlSafe(getHref()));
467             }
468 
469             if (triggerHashChanged) {
470                 final Event event = new HashChangeEvent(w, Event.TYPE_HASH_CHANGE, oldURL, getHref());
471                 w.executeEventLocally(event);
472             }
473         }
474     }
475 
476     /**
477      * Returns the hostname portion of the location URL.
478      *
479      * @return the hostname portion of the location URL
480      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hostname">MDN Documentation</a>
481      */
482     public String getHostname() {
483         return getUrl().getHost();
484     }
485 
486     /**
487      * Sets the hostname portion of the location URL.
488      *
489      * @param hostname the new hostname portion of the location URL
490      * @throws Exception if an error occurs
491      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hostname">MDN Documentation</a>
492      */
493     public void setHostname(final String hostname) throws Exception {
494         setUrl(UrlUtils.getUrlWithNewHost(getUrl(), hostname));
495     }
496 
497     /**
498      * Returns the host portion of the location URL (the {@code [hostname]:[port]} portion).
499      *
500      * @return the host portion of the location URL
501      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/host">MDN Documentation</a>
502      */
503     public String getHost() {
504         final URL url = getUrl();
505         final int port = url.getPort();
506         final String host = url.getHost();
507 
508         if (port == -1) {
509             return host;
510         }
511         return host + ":" + port;
512     }
513 
514     /**
515      * Sets the host portion of the location URL (the {@code [hostname]:[port]} portion).
516      *
517      * @param host the new host portion of the location URL
518      * @throws Exception if an error occurs
519      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/host">MDN Documentation</a>
520      */
521     public void setHost(final String host) throws Exception {
522         final String hostname;
523         final int port;
524         final int index = host.indexOf(':');
525         if (index == -1) {
526             hostname = host;
527             port = -1;
528         }
529         else {
530             hostname = host.substring(0, index);
531             port = Integer.parseInt(host.substring(index + 1));
532         }
533         final URL url = UrlUtils.getUrlWithNewHostAndPort(getUrl(), hostname, port);
534         setUrl(url);
535     }
536 
537     /**
538      * Returns the pathname portion of the location URL.
539      *
540      * @return the pathname portion of the location URL
541      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname">MDN Documentation</a>
542      */
543     public String getPathname() {
544         if (UrlUtils.URL_ABOUT_BLANK == getUrl()) {
545             return "blank";
546         }
547         return getUrl().getPath();
548     }
549 
550     /**
551      * Sets the pathname portion of the location URL.
552      *
553      * @param pathname the new pathname portion of the location URL
554      * @throws Exception if an error occurs
555      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname">MDN Documentation</a>
556      */
557     public void setPathname(final String pathname) throws Exception {
558         setUrl(UrlUtils.getUrlWithNewPath(getUrl(), pathname));
559     }
560 
561     /**
562      * Returns the port portion of the location URL.
563      *
564      * @return the port portion of the location URL, or an empty string if no port is specified
565      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/port">MDN Documentation</a>
566      */
567     public String getPort() {
568         final int port = getUrl().getPort();
569         if (port == -1) {
570             return "";
571         }
572         return Integer.toString(port);
573     }
574 
575     /**
576      * Sets the port portion of the location URL.
577      *
578      * @param port the new port portion of the location URL
579      * @throws Exception if an error occurs
580      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/port">MDN Documentation</a>
581      */
582     public void setPort(final String port) throws Exception {
583         setUrl(UrlUtils.getUrlWithNewPort(getUrl(), Integer.parseInt(port)));
584     }
585 
586     /**
587      * Returns the protocol portion of the location URL, including the trailing {@code :}.
588      *
589      * @return the protocol portion of the location URL
590      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/protocol">MDN Documentation</a>
591      */
592     public String getProtocol() {
593         return getUrl().getProtocol() + ":";
594     }
595 
596     /**
597      * Sets the protocol portion of the location URL.
598      *
599      * @param protocol the new protocol portion of the location URL
600      * @throws Exception if an error occurs
601      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/protocol">MDN Documentation</a>
602      */
603     public void setProtocol(final String protocol) throws Exception {
604         setUrl(UrlUtils.getUrlWithNewProtocol(getUrl(), protocol));
605     }
606 
607     /**
608      * Returns this location's current URL.
609      *
610      * @return this location's current URL
611      */
612     private URL getUrl() {
613         return window_.getWebWindow().getEnclosedPage().getUrl();
614     }
615 
616     /**
617      * Sets this location's URL, triggering a server request and loading the resulting document
618      * into this location's window.
619      *
620      * @param url this location's new URL
621      * @throws IOException if there is a problem loading the new location
622      */
623     private void setUrl(final URL url) throws IOException {
624         final WebWindow webWindow = window_.getWebWindow();
625         final BrowserVersion browserVersion = webWindow.getWebClient().getBrowserVersion();
626 
627         final WebRequest webRequest = new WebRequest(url,
628                 browserVersion.getHtmlAcceptHeader(), browserVersion.getAcceptEncodingHeader());
629         webRequest.setRefererHeader(getUrl());
630 
631         webWindow.getWebClient().getPage(webWindow, webRequest);
632     }
633 
634     /**
635      * Returns the {@code origin} property.
636      *
637      * @return the {@code origin} property
638      */
639     public String getOrigin() {
640         return getUrl().getProtocol() + "://" + getHost();
641     }
642 }