Statistics
| Branch: | Tag: | Revision:

root / web_client / src / gr / grnet / pithos / web / client / GSS.java @ 9e8e14e4

History | View | Annotate | Download (20.4 kB)

1
/*
2
 * Copyright (c) 2011 Greek Research and Technology Network
3
 */
4
package gr.grnet.pithos.web.client;
5

    
6
import com.google.gwt.core.client.Scheduler;
7
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
8
import com.google.gwt.user.client.ui.DockPanel;
9
import com.google.gwt.user.client.ui.HasVerticalAlignment;
10
import gr.grnet.pithos.web.client.clipboard.Clipboard;
11
import gr.grnet.pithos.web.client.commands.GetUserCommand;
12
import gr.grnet.pithos.web.client.foldertree.AccountResource;
13
import gr.grnet.pithos.web.client.foldertree.FolderTreeView;
14
import gr.grnet.pithos.web.client.rest.GetRequest;
15
import gr.grnet.pithos.web.client.rest.RestException;
16
import gr.grnet.pithos.web.client.rest.resource.FileResource;
17
import gr.grnet.pithos.web.client.rest.resource.OtherUserResource;
18
import gr.grnet.pithos.web.client.rest.resource.RestResource;
19
import gr.grnet.pithos.web.client.rest.resource.RestResourceWrapper;
20
import gr.grnet.pithos.web.client.rest.resource.SharedResource;
21
import gr.grnet.pithos.web.client.rest.resource.TrashResource;
22
import gr.grnet.pithos.web.client.rest.resource.UserResource;
23

    
24
import java.util.ArrayList;
25
import java.util.Arrays;
26
import java.util.Date;
27
import java.util.HashMap;
28
import java.util.List;
29

    
30
import com.google.gwt.core.client.EntryPoint;
31
import com.google.gwt.core.client.GWT;
32
import com.google.gwt.event.logical.shared.ResizeEvent;
33
import com.google.gwt.event.logical.shared.ResizeHandler;
34
import com.google.gwt.event.logical.shared.SelectionEvent;
35
import com.google.gwt.event.logical.shared.SelectionHandler;
36
import com.google.gwt.http.client.URL;
37
import com.google.gwt.i18n.client.DateTimeFormat;
38
import com.google.gwt.resources.client.ClientBundle;
39
import com.google.gwt.resources.client.ImageResource;
40
import com.google.gwt.user.client.Command;
41
import com.google.gwt.user.client.Cookies;
42
import com.google.gwt.user.client.DOM;
43
import com.google.gwt.user.client.DeferredCommand;
44
import com.google.gwt.user.client.Event;
45
import com.google.gwt.user.client.History;
46
import com.google.gwt.user.client.Window;
47
import com.google.gwt.user.client.ui.AbstractImagePrototype;
48
import com.google.gwt.user.client.ui.DecoratedTabPanel;
49
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
50
import com.google.gwt.user.client.ui.HorizontalSplitPanel;
51
import com.google.gwt.user.client.ui.RootPanel;
52
import com.google.gwt.user.client.ui.TabPanel;
53
import com.google.gwt.user.client.ui.VerticalPanel;
54

    
55
/**
56
 * Entry point classes define <code>onModuleLoad()</code>.
57
 */
58
public class GSS implements EntryPoint, ResizeHandler {
59

    
60
        /**
61
         * A constant that denotes the completion of an IncrementalCommand.
62
         */
63
        public static final boolean DONE = false;
64

    
65
        public static final int VISIBLE_FILE_COUNT = 25;
66

    
67
        /**
68
         * Instantiate an application-level image bundle. This object will provide
69
         * programmatic access to all the images needed by widgets.
70
         */
71
        private static Images images = (Images) GWT.create(Images.class);
72

    
73
    public String getUsername() {
74
        return username;
75
    }
76

    
77
    /**
78
         * An aggregate image bundle that pulls together all the images for this
79
         * application into a single bundle.
80
         */
81
        public interface Images extends ClientBundle, TopPanel.Images, StatusPanel.Images, FileMenu.Images, EditMenu.Images, SettingsMenu.Images, FilePropertiesDialog.Images, MessagePanel.Images, FileList.Images, Search.Images, CellTreeView.Images {
82

    
83
                @Source("gr/grnet/pithos/resources/document.png")
84
                ImageResource folders();
85

    
86
                @Source("gr/grnet/pithos/resources/edit_group_22.png")
87
                ImageResource groups();
88

    
89
                @Source("gr/grnet/pithos/resources/search.png")
90
                ImageResource search();
91
        }
92

    
93
        /**
94
         * The single GSS instance.
95
         */
96
        private static GSS singleton;
97

    
98
        /**
99
         * Gets the singleton GSS instance.
100
         *
101
         * @return the GSS object
102
         */
103
        public static GSS get() {
104
                if (GSS.singleton == null)
105
                        GSS.singleton = new GSS();
106
                return GSS.singleton;
107
        }
108

    
109
        /**
110
         * The Application Clipboard implementation;
111
         */
112
        private Clipboard clipboard = new Clipboard();
113

    
114
        private UserResource currentUserResource;
115

    
116
        /**
117
         * The top panel that contains the menu bar.
118
         */
119
        private TopPanel topPanel;
120

    
121
        /**
122
         * The panel that contains the various system messages.
123
         */
124
        private MessagePanel messagePanel = new MessagePanel(GSS.images);
125

    
126
        /**
127
         * The bottom panel that contains the status bar.
128
         */
129
        private StatusPanel statusPanel = new StatusPanel(GSS.images);
130

    
131
        /**
132
         * The top right panel that displays the logged in user details
133
         */
134
        private UserDetailsPanel userDetailsPanel = new UserDetailsPanel();
135

    
136
        /**
137
         * The file list widget.
138
         */
139
        private FileList fileList;
140

    
141
        /**
142
         * The tab panel that occupies the right side of the screen.
143
         */
144
        private TabPanel inner = new DecoratedTabPanel(){
145
                
146
                public void onBrowserEvent(com.google.gwt.user.client.Event event) {
147
                        if (DOM.eventGetType(event) == Event.ONCONTEXTMENU){
148
                                if(isFileListShowing()){
149
                                        getFileList().showContextMenu(event);
150
                                }
151
                        }
152
                };
153
        };
154

    
155
        /**
156
         * The split panel that will contain the left and right panels.
157
         */
158
        private HorizontalSplitPanel splitPanel = new HorizontalSplitPanel();
159

    
160
        /**
161
         * The horizontal panel that will contain the search and status panels.
162
         */
163
        private DockPanel searchStatus = new DockPanel();
164

    
165
        /**
166
         * The search widget.
167
         */
168
        private Search search;
169

    
170
        /**
171
         * The widget that displays the tree of folders.
172
         */
173
        
174
        private CellTreeView treeView = new CellTreeView(images);
175
        /**
176
         * The currently selected item in the application, for use by the Edit menu
177
         * commands. Potential types are Folder, File, User and Group.
178
         */
179
        private Object currentSelection;
180

    
181

    
182
        /**
183
         * The WebDAV password of the current user
184
         */
185
        private String webDAVPassword;
186

    
187
        public HashMap<String, String> userFullNameMap = new HashMap<String, String>();
188

    
189
    private String username = null;
190

    
191
    /**
192
     * The authentication token of the current user.
193
     */
194
    private String token;
195

    
196
    private FolderTreeView folderTreeView = new FolderTreeView();
197

    
198
        @Override
199
        public void onModuleLoad() {
200
                // Initialize the singleton before calling the constructors of the
201
                // various widgets that might call GSS.get().
202
                singleton = this;
203
                if (parseUserCredentials())
204
            initialize();
205
        }
206

    
207
    private void initialize() {
208
        topPanel = new TopPanel(GSS.images);
209
        topPanel.setWidth("100%");
210

    
211
        messagePanel.setWidth("100%");
212
        messagePanel.setVisible(false);
213

    
214
                search = new Search(images);
215
                searchStatus.add(search, DockPanel.WEST);
216
                searchStatus.add(userDetailsPanel, DockPanel.EAST);
217
                searchStatus.setCellHorizontalAlignment(userDetailsPanel, HasHorizontalAlignment.ALIGN_RIGHT);
218
                searchStatus.setCellVerticalAlignment(search, HasVerticalAlignment.ALIGN_MIDDLE);
219
                searchStatus.setCellVerticalAlignment(userDetailsPanel, HasVerticalAlignment.ALIGN_MIDDLE);
220
                searchStatus.setWidth("100%");
221

    
222
        fileList = new FileList(images);
223

    
224
        // Inner contains the various lists.
225
        inner.sinkEvents(Event.ONCONTEXTMENU);
226
        inner.setAnimationEnabled(true);
227
        inner.getTabBar().addStyleName("pithos-MainTabBar");
228
        inner.getDeckPanel().addStyleName("pithos-MainTabPanelBottom");
229
        inner.add(fileList, createHeaderHTML(AbstractImagePrototype.create(images.folders()), "Files"), true);
230

    
231
        inner.setWidth("100%");
232
        inner.selectTab(0);
233

    
234
        inner.addSelectionHandler(new SelectionHandler<Integer>() {
235

    
236
            @Override
237
            public void onSelection(SelectionEvent<Integer> event) {
238
                int tabIndex = event.getSelectedItem();
239
                switch (tabIndex) {
240
                    case 0:
241
                        fileList.updateCurrentlyShowingStats();
242
                        break;
243
                }
244
            }
245
        });
246

    
247
        // Add the left and right panels to the split panel.
248
        splitPanel.setLeftWidget(folderTreeView);
249
        splitPanel.setRightWidget(inner);
250
        splitPanel.setSplitPosition("25%");
251
        splitPanel.setSize("100%", "100%");
252
        splitPanel.addStyleName("pithos-splitPanel");
253

    
254
        // Create a dock panel that will contain the menu bar at the top,
255
        // the shortcuts to the left, the status bar at the bottom and the
256
        // right panel taking the rest.
257
        VerticalPanel outer = new VerticalPanel();
258
        outer.add(topPanel);
259
                outer.add(searchStatus);
260
        outer.add(messagePanel);
261
        outer.add(splitPanel);
262
        outer.add(statusPanel);
263
        outer.setWidth("100%");
264
        outer.setCellHorizontalAlignment(messagePanel, HasHorizontalAlignment.ALIGN_CENTER);
265

    
266
        outer.setSpacing(4);
267

    
268
        // Hook the window resize event, so that we can adjust the UI.
269
        Window.addResizeHandler(this);
270
        // Clear out the window's built-in margin, because we want to take
271
        // advantage of the entire client area.
272
        Window.setMargin("0px");
273
        // Finally, add the outer panel to the RootPanel, so that it will be
274
        // displayed.
275
        RootPanel.get().add(outer);
276
        // Call the window resized handler to get the initial sizes setup. Doing
277
        // this in a deferred command causes it to occur after all widgets'
278
        // sizes have been computed by the browser.
279
        DeferredCommand.addCommand(new Command() {
280

    
281
            @Override
282
            public void execute() {
283
                onWindowResized(Window.getClientHeight());
284
            }
285
        });
286
    }
287

    
288
        /**
289
         * Parse and store the user credentials to the appropriate fields.
290
         */
291
        private boolean parseUserCredentials() {
292
                Configuration conf = (Configuration) GWT.create(Configuration.class);
293
                String cookie = conf.authCookie();
294
                String auth = Cookies.getCookie(cookie);
295
                if (auth == null) {
296
                        authenticateUser();
297
            return false;
298
        }
299
        else {
300
            String[] authSplit = auth.split("\\" + conf.cookieSeparator(), 2);
301
            if (authSplit.length != 2) {
302
                authenticateUser();
303
                return false;
304
            }
305
            else {
306
                username = authSplit[0];
307
                token = authSplit[1];
308
                return true;
309
            }
310
        }
311
        }
312

    
313
    /**
314
         * Redirect the user to the login page for authentication.
315
         */
316
        protected void authenticateUser() {
317
                Configuration conf = (Configuration) GWT.create(Configuration.class);
318

    
319
//        Window.Location.assign(GWT.getModuleBaseURL() + conf.loginUrl() + "?next=" + Window.Location.getHref());
320
        Cookies.setCookie(conf.authCookie(), "demo" + conf.cookieSeparator() + "0000");
321
        Window.Location.assign(GWT.getModuleBaseURL() + "GSS.html");
322
        }
323

    
324
        /**
325
         * Clear the cookie and redirect the user to the logout page.
326
         */
327
        void logout() {
328
                Configuration conf = (Configuration) GWT.create(Configuration.class);
329
                String cookie = conf.authCookie();
330
                String domain = Window.Location.getHostName();
331
                String path = Window.Location.getPath();
332
                Cookies.setCookie(cookie, "", null, domain, path, false);
333
        String baseUrl = GWT.getModuleBaseURL();
334
        String homeUrl = baseUrl.substring(0, baseUrl.indexOf(path));
335
                Window.Location.assign(homeUrl + conf.logoutUrl());
336
        }
337

    
338
        /**
339
         * Creates an HTML fragment that places an image & caption together, for use
340
         * in a group header.
341
         *
342
         * @param imageProto an image prototype for an image
343
         * @param caption the group caption
344
         * @return the header HTML fragment
345
         */
346
        private String createHeaderHTML(AbstractImagePrototype imageProto, String caption) {
347
                String captionHTML = "<table class='caption' cellpadding='0' " 
348
                + "cellspacing='0'>" + "<tr><td class='lcaption'>" + imageProto.getHTML() 
349
                + "</td><td id =" + caption +" class='rcaption'><b style='white-space:nowrap'>&nbsp;" 
350
                + caption + "</b></td></tr></table>";
351
                return captionHTML;
352
        }
353

    
354
        private void onWindowResized(int height) {
355
                // Adjust the split panel to take up the available room in the window.
356
                int newHeight = height - splitPanel.getAbsoluteTop() - 44;
357
                if (newHeight < 1)
358
                        newHeight = 1;
359
                splitPanel.setHeight("" + newHeight);
360
                inner.setHeight("" + newHeight);
361
        }
362

    
363
        @Override
364
        public void onResize(ResizeEvent event) {
365
                int height = event.getHeight();
366
                onWindowResized(height);
367
        }
368

    
369
        public boolean isFileListShowing() {
370
                int tab = inner.getTabBar().getSelectedTab();
371
                if (tab == 0)
372
                        return true;
373
                return false;
374
        }
375

    
376
        public boolean isSearchResultsShowing() {
377
                int tab = inner.getTabBar().getSelectedTab();
378
                if (tab == 2)
379
                        return true;
380
                return false;
381
        }
382

    
383
        /**
384
         * Make the user list visible.
385
         */
386
        public void showUserList() {
387
                inner.selectTab(1);
388
        }
389

    
390
        /**
391
         * Make the file list visible.
392
         */
393
        public void showFileList() {
394
                fileList.updateFileCache(true /*clear selection*/);
395
                inner.selectTab(0);
396
        }
397

    
398
        /**
399
         * Make the file list visible.
400
         *
401
         * @param update
402
         */
403
        public void showFileList(boolean update) {
404
                if(update){
405
                        getTreeView().refreshCurrentNode(true);
406
                }
407
                else{
408
                        RestResource currentFolder = getTreeView().getSelection();
409
                        if(currentFolder!=null){
410
                                showFileList(currentFolder);
411
                }
412
                }
413

    
414
        }
415
        
416
        public void showFileList(RestResource r) {
417
                showFileList(r,true);
418
        }
419
        
420
        public void showFileList(RestResource r, boolean clearSelection) {
421
                RestResource currentFolder = r;
422
                if(currentFolder!=null){
423
                        List<FileResource> files = null;
424
                        if (currentFolder instanceof RestResourceWrapper) {
425
                                RestResourceWrapper folder = (RestResourceWrapper) currentFolder;
426
                                files = folder.getResource().getFiles();
427
                        } else if (currentFolder instanceof TrashResource) {
428
                                TrashResource folder = (TrashResource) currentFolder;
429
                                files = folder.getFiles();
430
                        }
431
                        else if (currentFolder instanceof SharedResource) {
432
                                SharedResource folder = (SharedResource) currentFolder;
433
                                files = folder.getFiles();
434
                        }
435
                        else if (currentFolder instanceof OtherUserResource) {
436
                                OtherUserResource folder = (OtherUserResource) currentFolder;
437
                                files = folder.getFiles();
438
                        }
439
                        if (files != null)
440
                                getFileList().setFiles(files);
441
                        else
442
                                getFileList().setFiles(new ArrayList<FileResource>());
443
                }
444
                fileList.updateFileCache(clearSelection /*clear selection*/);
445
                inner.selectTab(0);
446
        }
447

    
448
        /**
449
         * Display the 'loading' indicator.
450
         */
451
        public void showLoadingIndicator(String message, String path) {
452
                if(path!=null){
453
                        String[] split = path.split("/");
454
                        message = message +" "+URL.decode(split[split.length-1]);
455
                }
456
                topPanel.getLoading().show(message);
457
        }
458

    
459
        /**
460
         * Hide the 'loading' indicator.
461
         */
462
        public void hideLoadingIndicator() {
463
                topPanel.getLoading().hide();
464
        }
465

    
466
        /**
467
         * A native JavaScript method to reach out to the browser's window and
468
         * invoke its resizeTo() method.
469
         *
470
         * @param x the new width
471
         * @param y the new height
472
         */
473
        public static native void resizeTo(int x, int y) /*-{
474
                $wnd.resizeTo(x,y);
475
        }-*/;
476

    
477
        /**
478
         * A helper method that returns true if the user's list is currently visible
479
         * and false if it is hidden.
480
         *
481
         * @return true if the user list is visible
482
         */
483
        public boolean isUserListVisible() {
484
                return inner.getTabBar().getSelectedTab() == 1;
485
        }
486

    
487
        /**
488
         * Display an error message.
489
         *
490
         * @param msg the message to display
491
         */
492
        public void displayError(String msg) {
493
                messagePanel.displayError(msg);
494
        }
495

    
496
        /**
497
         * Display a warning message.
498
         *
499
         * @param msg the message to display
500
         */
501
        public void displayWarning(String msg) {
502
                messagePanel.displayWarning(msg);
503
        }
504

    
505
        /**
506
         * Display an informational message.
507
         *
508
         * @param msg the message to display
509
         */
510
        public void displayInformation(String msg) {
511
                messagePanel.displayInformation(msg);
512
        }
513

    
514
        /**
515
         * Retrieve the folders.
516
         *
517
         * @return the folders
518
         
519
        public Folders getFolders() {
520
                return folders;
521
        }*/
522

    
523
        /**
524
         * Retrieve the search.
525
         *
526
         * @return the search
527
         */
528
        Search getSearch() {
529
                return search;
530
        }
531

    
532
        /**
533
         * Retrieve the currentSelection.
534
         *
535
         * @return the currentSelection
536
         */
537
        public Object getCurrentSelection() {
538
                return currentSelection;
539
        }
540

    
541
        /**
542
         * Modify the currentSelection.
543
         *
544
         * @param newCurrentSelection the currentSelection to set
545
         */
546
        public void setCurrentSelection(Object newCurrentSelection) {
547
                currentSelection = newCurrentSelection;
548
        }
549

    
550
        /**
551
         * Retrieve the fileList.
552
         *
553
         * @return the fileList
554
         */
555
        public FileList getFileList() {
556
                return fileList;
557
        }
558

    
559
        /**
560
         * Retrieve the topPanel.
561
         *
562
         * @return the topPanel
563
         */
564
        TopPanel getTopPanel() {
565
                return topPanel;
566
        }
567

    
568
        /**
569
         * Retrieve the clipboard.
570
         *
571
         * @return the clipboard
572
         */
573
        public Clipboard getClipboard() {
574
                return clipboard;
575
        }
576

    
577
        public StatusPanel getStatusPanel() {
578
                return statusPanel;
579
        }
580

    
581
        /**
582
         * Retrieve the userDetailsPanel.
583
         *
584
         * @return the userDetailsPanel
585
         */
586
        public UserDetailsPanel getUserDetailsPanel() {
587
                return userDetailsPanel;
588
        }
589

    
590
        
591

    
592
        public String getToken() {
593
                return token;
594
        }
595

    
596
        public String getWebDAVPassword() {
597
                return webDAVPassword;
598
        }
599

    
600
        /**
601
         * Retrieve the currentUserResource.
602
         *
603
         * @return the currentUserResource
604
         */
605
        public UserResource getCurrentUserResource() {
606
                return currentUserResource;
607
        }
608

    
609
        /**
610
         * Modify the currentUserResource.
611
         *
612
         * @param newUser the new currentUserResource
613
         */
614
        public void setCurrentUserResource(UserResource newUser) {
615
                currentUserResource = newUser;
616
        }
617

    
618
        public static native void preventIESelection() /*-{
619
                $doc.body.onselectstart = function () { return false; };
620
        }-*/;
621

    
622
        public static native void enableIESelection() /*-{
623
                if ($doc.body.onselectstart != null)
624
                $doc.body.onselectstart = null;
625
        }-*/;
626

    
627
        /**
628
         * @return the absolute path of the API root URL
629
         */
630
        public String getApiPath() {
631
                Configuration conf = (Configuration) GWT.create(Configuration.class);
632
                return conf.apiPath();
633
        }
634

    
635
        /**
636
         * Convert server date to local time according to browser timezone
637
         * and format it according to localized pattern.
638
         * Time is always formatted to 24hr format.
639
         * NB: This assumes that server runs in UTC timezone. Otherwise
640
         * we would need to adjust for server time offset as well.
641
         *
642
         * @param date
643
         * @return String
644
         */
645
        public static String formatLocalDateTime(Date date) {
646
                Date convertedDate = new Date(date.getTime() - date.getTimezoneOffset());
647
                final DateTimeFormat dateFormatter = DateTimeFormat.getShortDateFormat();
648
                final DateTimeFormat timeFormatter = DateTimeFormat.getFormat("HH:mm");
649
                String datePart = dateFormatter.format(convertedDate);
650
                String timePart = timeFormatter.format(convertedDate);
651
                return datePart + " " + timePart;
652
        }
653
        
654
        /**
655
         * History support for folder navigation
656
         * adds a new browser history entry
657
         *
658
         * @param key
659
         */
660
        public void updateHistory(String key){
661
//                Replace any whitespace of the initial string to "+"
662
//                String result = key.replaceAll("\\s","+");
663
//                Add a new browser history entry.
664
//                History.newItem(result);
665
                History.newItem(key);
666
        }
667

    
668
        /**
669
         * This method examines the token input and add a "/" at the end in case it's omitted.
670
         * This happens only in Files/trash/, Files/shared/, Files/others.
671
         *
672
         * @param tokenInput
673
         * @return the formated token with a "/" at the end or the same tokenInput parameter
674
         */
675

    
676
        private String handleSpecialFolderNames(String tokenInput){
677
                List<String> pathsToCheck = Arrays.asList("Files/trash", "Files/shared", "Files/others");
678
                if(pathsToCheck.contains(tokenInput))
679
                        return tokenInput + "/";
680
                return tokenInput;
681

    
682
        }
683

    
684
        /**
685
         * Reject illegal resource names, like '.' or '..' or slashes '/'.
686
         */
687
        static boolean isValidResourceName(String name) {
688
                if (".".equals(name) ||        "..".equals(name) || name.contains("/"))
689
                        return false;
690
                return true;
691
        }
692

    
693
        public void putUserToMap(String _userName, String _userFullName){
694
                userFullNameMap.put(_userName, _userFullName);
695
        }
696

    
697
        public String findUserFullName(String _userName){
698
                return userFullNameMap.get(_userName);
699
        }
700
        public String getUserFullName(String _userName) {
701
                
702
        if (GSS.get().findUserFullName(_userName) == null)
703
                //if there is no userFullName found then the map fills with the given _userName,
704
                //so userFullName = _userName
705
                GSS.get().putUserToMap(_userName, _userName);
706
        else if(GSS.get().findUserFullName(_userName).indexOf('@') != -1){
707
                //if the userFullName = _userName the GetUserCommand updates the userFullName in the map
708
                GetUserCommand guc = new GetUserCommand(_userName);
709
                guc.execute();
710
        }
711
        return GSS.get().findUserFullName(_userName);
712
        }
713
        /**
714
         * Retrieve the treeView.
715
         *
716
         * @return the treeView
717
         */
718
        public CellTreeView getTreeView() {
719
                return treeView;
720
        }
721
        
722
        public void onResourceUpdate(RestResource resource,boolean clearSelection){
723
                if(resource instanceof RestResourceWrapper || resource instanceof OtherUserResource || resource instanceof TrashResource || resource instanceof SharedResource){
724
                        if(getTreeView().getSelection()!=null&&getTreeView().getSelection().getUri().equals(resource.getUri()))
725
                                showFileList(resource,clearSelection);
726
                }
727
                
728
        }
729
}