001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.hbtop.screen.top;
019
020import edu.umd.cs.findbugs.annotations.Nullable;
021import java.util.ArrayList;
022import java.util.EnumMap;
023import java.util.List;
024import java.util.Objects;
025import java.util.concurrent.atomic.AtomicBoolean;
026import java.util.concurrent.atomic.AtomicLong;
027import java.util.stream.Collectors;
028import org.apache.hadoop.hbase.hbtop.Record;
029import org.apache.hadoop.hbase.hbtop.RecordFilter;
030import org.apache.hadoop.hbase.hbtop.field.Field;
031import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
032import org.apache.hadoop.hbase.hbtop.mode.Mode;
033import org.apache.hadoop.hbase.hbtop.screen.Screen;
034import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
035import org.apache.hadoop.hbase.hbtop.screen.field.FieldScreenView;
036import org.apache.hadoop.hbase.hbtop.screen.help.HelpScreenView;
037import org.apache.hadoop.hbase.hbtop.screen.mode.ModeScreenView;
038import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
039import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
040import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
041import org.apache.yetus.audience.InterfaceAudience;
042
043/**
044 * The presentation logic for the top screen.
045 */
046@InterfaceAudience.Private
047public class TopScreenPresenter {
048  private final TopScreenView topScreenView;
049  private final AtomicLong refreshDelay;
050  private long lastRefreshTimestamp;
051
052  private final AtomicBoolean adjustFieldLength = new AtomicBoolean(true);
053  private final TopScreenModel topScreenModel;
054  private int terminalLength;
055  private int horizontalScroll;
056  private final Paging paging = new Paging();
057
058  private final EnumMap<Field, Boolean> fieldDisplayMap = new EnumMap<>(Field.class);
059  private final EnumMap<Field, Integer> fieldLengthMap = new EnumMap<>(Field.class);
060
061  private final long numberOfIterations;
062  private long iterations;
063
064  public TopScreenPresenter(TopScreenView topScreenView, long initialRefreshDelay,
065    TopScreenModel topScreenModel, @Nullable List<Field> initialFields, long numberOfIterations) {
066    this.topScreenView = Objects.requireNonNull(topScreenView);
067    this.refreshDelay = new AtomicLong(initialRefreshDelay);
068    this.topScreenModel = Objects.requireNonNull(topScreenModel);
069    this.numberOfIterations = numberOfIterations;
070
071    initFieldDisplayMapAndFieldLengthMap(initialFields);
072  }
073
074  public void init() {
075    updateTerminalLengthAndPageSize(topScreenView.getTerminalSize(), topScreenView.getPageSize());
076    topScreenView.hideCursor();
077  }
078
079  private void updateTerminalLengthAndPageSize(@Nullable TerminalSize terminalSize,
080    @Nullable Integer pageSize) {
081    if (terminalSize != null) {
082      terminalLength = terminalSize.getColumns();
083    } else {
084      terminalLength = Integer.MAX_VALUE;
085    }
086    if (pageSize != null) {
087      paging.updatePageSize(pageSize);
088    } else {
089      paging.updatePageSize(Integer.MAX_VALUE);
090    }
091  }
092
093  public long refresh(boolean force) {
094    if (!force) {
095      long delay = EnvironmentEdgeManager.currentTime() - lastRefreshTimestamp;
096      if (delay < refreshDelay.get()) {
097        return refreshDelay.get() - delay;
098      }
099    }
100
101    TerminalSize newTerminalSize = topScreenView.doResizeIfNecessary();
102    if (newTerminalSize != null) {
103      updateTerminalLengthAndPageSize(newTerminalSize, topScreenView.getPageSize());
104      topScreenView.clearTerminal();
105    }
106
107    topScreenModel.refreshMetricsData();
108    paging.updateRecordsSize(topScreenModel.getRecords().size());
109
110    adjustFieldLengthIfNeeded();
111
112    topScreenView.showTopScreen(topScreenModel.getSummary(), getDisplayedHeaders(),
113      getDisplayedRecords(), getSelectedRecord());
114
115    topScreenView.refreshTerminal();
116
117    lastRefreshTimestamp = EnvironmentEdgeManager.currentTime();
118    iterations++;
119    return refreshDelay.get();
120  }
121
122  public void adjustFieldLength() {
123    adjustFieldLength.set(true);
124    refresh(true);
125  }
126
127  private void adjustFieldLengthIfNeeded() {
128    if (adjustFieldLength.get()) {
129      adjustFieldLength.set(false);
130
131      for (Field f : topScreenModel.getFields()) {
132        if (f.isAutoAdjust()) {
133          int maxLength = topScreenModel.getRecords().stream()
134            .map(r -> r.get(f).asString().length()).max(Integer::compareTo).orElse(0);
135          fieldLengthMap.put(f, Math.max(maxLength, f.getHeader().length()));
136        }
137      }
138    }
139  }
140
141  private List<Header> getDisplayedHeaders() {
142    List<Field> displayFields =
143      topScreenModel.getFields().stream().filter(fieldDisplayMap::get).collect(Collectors.toList());
144
145    if (displayFields.isEmpty()) {
146      horizontalScroll = 0;
147    } else if (horizontalScroll > displayFields.size() - 1) {
148      horizontalScroll = displayFields.size() - 1;
149    }
150
151    List<Header> ret = new ArrayList<>();
152
153    int length = 0;
154    for (int i = horizontalScroll; i < displayFields.size(); i++) {
155      Field field = displayFields.get(i);
156      int fieldLength = fieldLengthMap.get(field);
157
158      length += fieldLength + 1;
159      if (length > terminalLength) {
160        break;
161      }
162      ret.add(new Header(field, fieldLength));
163    }
164
165    return ret;
166  }
167
168  private List<Record> getDisplayedRecords() {
169    List<Record> ret = new ArrayList<>();
170    for (int i = paging.getPageStartPosition(); i < paging.getPageEndPosition(); i++) {
171      ret.add(topScreenModel.getRecords().get(i));
172    }
173    return ret;
174  }
175
176  private Record getSelectedRecord() {
177    if (topScreenModel.getRecords().isEmpty()) {
178      return null;
179    }
180    return topScreenModel.getRecords().get(paging.getCurrentPosition());
181  }
182
183  public void arrowUp() {
184    paging.arrowUp();
185    refresh(true);
186  }
187
188  public void arrowDown() {
189    paging.arrowDown();
190    refresh(true);
191  }
192
193  public void pageUp() {
194    paging.pageUp();
195    refresh(true);
196  }
197
198  public void pageDown() {
199    paging.pageDown();
200    refresh(true);
201  }
202
203  public void arrowLeft() {
204    if (horizontalScroll > 0) {
205      horizontalScroll -= 1;
206    }
207    refresh(true);
208  }
209
210  public void arrowRight() {
211    if (horizontalScroll < getHeaderSize() - 1) {
212      horizontalScroll += 1;
213    }
214    refresh(true);
215  }
216
217  public void home() {
218    if (horizontalScroll > 0) {
219      horizontalScroll = 0;
220    }
221    refresh(true);
222  }
223
224  public void end() {
225    int headerSize = getHeaderSize();
226    horizontalScroll = headerSize == 0 ? 0 : headerSize - 1;
227    refresh(true);
228  }
229
230  private int getHeaderSize() {
231    return (int) topScreenModel.getFields().stream().filter(fieldDisplayMap::get).count();
232  }
233
234  public void switchSortOrder() {
235    topScreenModel.switchSortOrder();
236    refresh(true);
237  }
238
239  public ScreenView transitionToHelpScreen(Screen screen, Terminal terminal) {
240    return new HelpScreenView(screen, terminal, refreshDelay.get(), topScreenView);
241  }
242
243  public ScreenView transitionToModeScreen(Screen screen, Terminal terminal) {
244    return new ModeScreenView(screen, terminal, topScreenModel.getCurrentMode(), this::switchMode,
245      topScreenView);
246  }
247
248  public ScreenView transitionToFieldScreen(Screen screen, Terminal terminal) {
249    return new FieldScreenView(screen, terminal, topScreenModel.getCurrentSortField(),
250      topScreenModel.getFields(), fieldDisplayMap, (sortField, fields, fieldDisplayMap) -> {
251        topScreenModel.setSortFieldAndFields(sortField, fields);
252        this.fieldDisplayMap.clear();
253        this.fieldDisplayMap.putAll(fieldDisplayMap);
254      }, topScreenView);
255  }
256
257  private void switchMode(Mode nextMode) {
258    topScreenModel.switchMode(nextMode, false, null);
259    reset();
260  }
261
262  public void drillDown() {
263    Record selectedRecord = getSelectedRecord();
264    if (selectedRecord == null) {
265      return;
266    }
267    if (topScreenModel.drillDown(selectedRecord)) {
268      reset();
269      refresh(true);
270    }
271  }
272
273  private void reset() {
274    initFieldDisplayMapAndFieldLengthMap(null);
275    adjustFieldLength.set(true);
276    paging.init();
277    horizontalScroll = 0;
278    topScreenView.clearTerminal();
279  }
280
281  private void initFieldDisplayMapAndFieldLengthMap(@Nullable List<Field> initialFields) {
282    fieldDisplayMap.clear();
283    fieldLengthMap.clear();
284    for (FieldInfo fieldInfo : topScreenModel.getFieldInfos()) {
285      if (initialFields != null) {
286        fieldDisplayMap.put(fieldInfo.getField(), initialFields.contains(fieldInfo.getField()));
287      } else {
288        fieldDisplayMap.put(fieldInfo.getField(), fieldInfo.isDisplayByDefault());
289      }
290      fieldLengthMap.put(fieldInfo.getField(), fieldInfo.getDefaultLength());
291    }
292  }
293
294  public ScreenView goToMessageMode(Screen screen, Terminal terminal, int row, String message) {
295    return new MessageModeScreenView(screen, terminal, row, message, topScreenView);
296  }
297
298  public ScreenView goToInputModeForRefreshDelay(Screen screen, Terminal terminal, int row) {
299    return new InputModeScreenView(screen, terminal, row,
300      "Change refresh delay from " + (double) refreshDelay.get() / 1000 + " to", null,
301      (inputString) -> {
302        if (inputString.isEmpty()) {
303          return topScreenView;
304        }
305
306        double delay;
307        try {
308          delay = Double.parseDouble(inputString);
309        } catch (NumberFormatException e) {
310          return goToMessageMode(screen, terminal, row, "Unacceptable floating point");
311        }
312
313        refreshDelay.set((long) (delay * 1000));
314        return topScreenView;
315      });
316  }
317
318  public ScreenView goToInputModeForFilter(Screen screen, Terminal terminal, int row,
319    boolean ignoreCase) {
320    return new InputModeScreenView(screen, terminal, row,
321      "add filter #" + (topScreenModel.getFilters().size() + 1) + " ("
322        + (ignoreCase ? "ignoring case" : "case sensitive") + ") as: [!]FLD?VAL",
323      topScreenModel.getFilterHistories(), (inputString) -> {
324        if (inputString.isEmpty()) {
325          return topScreenView;
326        }
327
328        if (!topScreenModel.addFilter(inputString, ignoreCase)) {
329          return goToMessageMode(screen, terminal, row, "Unacceptable filter expression");
330        }
331
332        paging.init();
333        return topScreenView;
334      });
335  }
336
337  public void clearFilters() {
338    topScreenModel.clearFilters();
339    paging.init();
340    refresh(true);
341  }
342
343  public ScreenView goToFilterDisplayMode(Screen screen, Terminal terminal, int row) {
344    ArrayList<RecordFilter> filters = new ArrayList<>(topScreenModel.getFilters());
345    filters.addAll(topScreenModel.getPushDownFilters());
346    return new FilterDisplayModeScreenView(screen, terminal, row, filters, topScreenView);
347  }
348
349  public boolean isIterationFinished() {
350    return iterations >= numberOfIterations;
351  }
352}