1 /*
2  * ChooseMirrorDialog.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.common.mirrors;
16 
17 import java.util.ArrayList;
18 
19 import com.google.gwt.aria.client.Roles;
20 import org.rstudio.core.client.Debug;
21 import org.rstudio.core.client.StringUtil;
22 import org.rstudio.core.client.widget.FormLabel;
23 import org.rstudio.core.client.widget.ModalDialog;
24 import org.rstudio.core.client.widget.OperationWithInput;
25 import org.rstudio.core.client.widget.ProgressIndicator;
26 import org.rstudio.core.client.widget.SimplePanelWithProgress;
27 import org.rstudio.core.client.widget.images.ProgressImages;
28 import org.rstudio.studio.client.common.GlobalDisplay;
29 import org.rstudio.studio.client.common.SimpleRequestCallback;
30 import org.rstudio.studio.client.common.mirrors.model.CRANMirror;
31 import org.rstudio.studio.client.common.mirrors.model.MirrorsServerOperations;
32 import org.rstudio.studio.client.server.ServerDataSource;
33 import org.rstudio.studio.client.server.ServerError;
34 import org.rstudio.studio.client.server.ServerRequestCallback;
35 
36 import com.google.gwt.core.client.GWT;
37 import com.google.gwt.core.client.JsArray;
38 import com.google.gwt.dom.client.Style.Unit;
39 import com.google.gwt.event.dom.client.DoubleClickEvent;
40 import com.google.gwt.event.dom.client.DoubleClickHandler;
41 import com.google.gwt.resources.client.ClientBundle;
42 import com.google.gwt.resources.client.CssResource;
43 import com.google.gwt.user.client.ui.ListBox;
44 import com.google.gwt.user.client.ui.TextBox;
45 import com.google.gwt.user.client.ui.VerticalPanel;
46 import com.google.gwt.user.client.ui.Widget;
47 
48 public class ChooseMirrorDialog extends ModalDialog<CRANMirror>
49 {
50    public interface Source
51                     extends ServerDataSource<JsArray<CRANMirror>>
52    {
getLabel(CRANMirror mirror)53       String getLabel(CRANMirror mirror);
getURL(CRANMirror mirror)54       String getURL(CRANMirror mirror);
55    }
56 
ChooseMirrorDialog(GlobalDisplay globalDisplay, Source mirrorSource, OperationWithInput<CRANMirror> inputOperation, MirrorsServerOperations mirrorOperations)57    public ChooseMirrorDialog(GlobalDisplay globalDisplay,
58                              Source mirrorSource,
59                              OperationWithInput<CRANMirror> inputOperation,
60                              MirrorsServerOperations mirrorOperations)
61    {
62       super("Retrieving list of CRAN mirrors...", Roles.getDialogRole(), inputOperation);
63       globalDisplay_ = globalDisplay;
64       mirrorSource_ = mirrorSource;
65       mirrorOperations_ = mirrorOperations;
66       progressIndicator_ = addProgressIndicator(false);
67    }
68 
69    @Override
collectInput()70    protected CRANMirror collectInput()
71    {
72       if (!StringUtil.isNullOrEmpty(customTextBox_.getText()))
73       {
74          CRANMirror cranMirror = CRANMirror.empty();
75          cranMirror.setURL(customTextBox_.getText());
76 
77          cranMirror.setAsCustom();
78 
79          return cranMirror;
80       }
81       else if (listBox_ != null && listBox_.getSelectedIndex() >= 0)
82       {
83          return mirrors_.get(listBox_.getSelectedIndex());
84       }
85       else
86       {
87          return null;
88       }
89    }
90 
validateSync(CRANMirror input)91    private boolean validateSync(CRANMirror input)
92    {
93       if (input == null)
94       {
95          globalDisplay_.showErrorMessage("Error",
96                                          "Please select a CRAN Mirror");
97          return false;
98       }
99       else
100       {
101          return true;
102       }
103    }
104 
105    @Override
validateAsync(CRANMirror input, OperationWithInput<Boolean> onValidated)106    protected void validateAsync(CRANMirror input,
107          OperationWithInput<Boolean> onValidated)
108    {
109       if (!validateSync(input))
110       {
111          onValidated.execute(false);
112          return;
113       }
114 
115       if (input.isCustom())
116       {
117          progressIndicator_.onProgress("Validating CRAN repository...");
118 
119          mirrorOperations_.validateCranRepo(
120             new ServerRequestCallback<Boolean>()
121             {
122                public void onResponseReceived(Boolean validated)
123                {
124                   progressIndicator_.onCompleted();
125 
126                   if (!validated)
127                   {
128                      progressIndicator_.onError("The given URL does not appear to be a valid CRAN repository");
129                      onValidated.execute(false);
130                   }
131                   else
132                   {
133                      onValidated.execute(true);
134                   }
135                }
136 
137                @Override
138                public void onError(ServerError error)
139                {
140                   progressIndicator_.onCompleted();
141 
142                   Debug.logError(error);
143                   progressIndicator_.onError(error.getMessage());
144 
145                   onValidated.execute(false);
146                }
147             },
148             input.getURL());
149       }
150       else
151       {
152          onValidated.execute(true);
153       }
154    }
155 
156    @Override
createMainWidget()157    protected Widget createMainWidget()
158    {
159       VerticalPanel root = new VerticalPanel();
160 
161       customTextBox_ = new TextBox();
162       customTextBox_.setStylePrimaryName(RESOURCES.styles().customRepo());
163       FormLabel customLabel = new FormLabel("Custom:", customTextBox_);
164       root.add(customLabel);
165       root.add(customTextBox_);
166 
167       FormLabel mirrorsLabel = new FormLabel("CRAN Mirrors:");
168       mirrorsLabel.getElement().getStyle().setMarginTop(8, Unit.PX);
169       root.add(mirrorsLabel);
170 
171       // create progress container
172       final SimplePanelWithProgress panel = new SimplePanelWithProgress(
173                                           ProgressImages.createLargeGray());
174       root.add(panel);
175 
176       panel.setStylePrimaryName(RESOURCES.styles().mainWidget());
177 
178       // show progress (with delay)
179       panel.showProgress(200);
180 
181       // query data source for packages
182       mirrorSource_.requestData(new SimpleRequestCallback<JsArray<CRANMirror>>() {
183 
184          @Override
185          public void onResponseReceived(JsArray<CRANMirror> mirrors)
186          {
187             // keep internal list of mirrors
188             mirrors_ = new ArrayList<>(mirrors.length());
189 
190             // create list box and select default item
191             listBox_ = new ListBox();
192             listBox_.setMultipleSelect(false);
193             listBox_.setVisibleItemCount(18); // all
194             listBox_.setWidth("100%");
195             if (mirrors.length() > 0)
196             {
197                for(int i=0; i<mirrors.length(); i++)
198                {
199                   CRANMirror mirror = mirrors.get(i);
200                   if (mirrorSource_.getLabel(mirror).startsWith("0-Cloud"))
201                      continue;
202                   mirrors_.add(mirror);
203                   String item = mirrorSource_.getLabel(mirror);
204                   String value = mirrorSource_.getURL(mirror);
205 
206                   listBox_.addItem(item, value);
207                }
208 
209                listBox_.setSelectedIndex(0);
210             }
211 
212             mirrorsLabel.setFor(listBox_);
213 
214             // set it into the panel
215             panel.setWidget(listBox_);
216 
217             // set caption
218             setText("Choose Primary Repository");
219 
220             // update ok button on changed
221             listBox_.addDoubleClickHandler(new DoubleClickHandler() {
222                @Override
223                public void onDoubleClick(DoubleClickEvent event)
224                {
225                   clickOkButton();
226                }
227             });
228 
229             // if the list box is larger than the space we initially allocated
230             // then increase the panel height
231             final int kDefaultPanelHeight = 265;
232             if (listBox_.getOffsetHeight() > kDefaultPanelHeight)
233                panel.setHeight(listBox_.getOffsetHeight() + "px");
234          }
235 
236          @Override
237          public void onError(ServerError error)
238          {
239             closeDialog();
240             super.onError(error);
241          }
242       });
243 
244       return root;
245    }
246 
247    static interface Styles extends CssResource
248    {
mainWidget()249       String mainWidget();
customRepo()250       String customRepo();
251    }
252 
253    static interface Resources extends ClientBundle
254    {
255       @Source("ChooseMirrorDialog.css")
styles()256       Styles styles();
257    }
258 
259    static Resources RESOURCES = (Resources)GWT.create(Resources.class);
ensureStylesInjected()260    public static void ensureStylesInjected()
261    {
262       RESOURCES.styles().ensureInjected();
263    }
264 
265    private final GlobalDisplay globalDisplay_;
266    private final Source mirrorSource_;
267    private ArrayList<CRANMirror> mirrors_ = null;
268    private ListBox listBox_ = null;
269    private TextBox customTextBox_ = null;
270    private final MirrorsServerOperations mirrorOperations_;
271    private final ProgressIndicator progressIndicator_;
272 }
273