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 java.util.ArrayList;
021import java.util.EnumMap;
022import java.util.List;
023import java.util.Objects;
024import java.util.concurrent.atomic.AtomicBoolean;
025import java.util.concurrent.atomic.AtomicLong;
026import java.util.stream.Collectors;
027import org.apache.hadoop.hbase.hbtop.Record;
028import org.apache.hadoop.hbase.hbtop.field.Field;
029import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
030import org.apache.hadoop.hbase.hbtop.mode.Mode;
031import org.apache.hadoop.hbase.hbtop.screen.Screen;
032import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
033import org.apache.hadoop.hbase.hbtop.screen.field.FieldScreenView;
034import org.apache.hadoop.hbase.hbtop.screen.help.HelpScreenView;
035import org.apache.hadoop.hbase.hbtop.screen.mode.ModeScreenView;
036import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
037import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
038import org.apache.yetus.audience.InterfaceAudience;
039
040
041/**
042 * The presentation logic for the top screen.
043 */
044@InterfaceAudience.Private
045public class TopScreenPresenter {
046  private final TopScreenView topScreenView;
047  private final AtomicLong refreshDelay;
048  private long lastRefreshTimestamp;
049
050  private final AtomicBoolean adjustFieldLength = new AtomicBoolean(true);
051  private final TopScreenModel topScreenModel;
052  private int terminalLength;
053  private int horizontalScroll;
054  private final Paging paging = new Paging();
055
056  private final EnumMap<Field, Boolean> fieldDisplayMap = new EnumMap<>(Field.class);
057  private final EnumMap<Field, Integer> fieldLengthMap = new EnumMap<>(Field.class);
058
059  public TopScreenPresenter(TopScreenView topScreenView, long initialRefreshDelay,
060    TopScreenModel topScreenModel) {
061    this.topScreenView = Objects.requireNonNull(topScreenView);
062    this.refreshDelay = new AtomicLong(initialRefreshDelay);
063    this.topScreenModel = Objects.requireNonNull(topScreenModel);
064
065    initFieldDisplayMapAndFieldLengthMap();
066  }
067
068  public void init() {
069    terminalLength = topScreenView.getTerminalSize().getColumns();
070    paging.updatePageSize(topScreenView.getPageSize());
071    topScreenView.hideCursor();
072  }
073
074  public long refresh(boolean force) {
075    if (!force) {
076      long delay = System.currentTimeMillis() - lastRefreshTimestamp;
077      if (delay < refreshDelay.get()) {
078        return refreshDelay.get() - delay;
079      }
080    }
081
082    TerminalSize newTerminalSize = topScreenView.doResizeIfNecessary();
083    if (newTerminalSize != null) {
084      terminalLength = newTerminalSize.getColumns();
085      paging.updatePageSize(topScreenView.getPageSize());
086      topScreenView.clearTerminal();
087    }
088
089    topScreenModel.refreshMetricsData();
090    paging.updateRecordsSize(topScreenModel.getRecords().size());
091
092    adjustFieldLengthIfNeeded();
093
094    topScreenView.showTopScreen(topScreenModel.getSummary(), getDisplayedHeaders(),
095      getDisplayedRecords(), getSelectedRecord());
096
097    topScreenView.refreshTerminal();
098
099    lastRefreshTimestamp = System.currentTimeMillis();
100    return refreshDelay.get();
101  }
102
103  public void adjustFieldLength() {
104    adjustFieldLength.set(true);
105    refresh(true);
106  }
107
108  private void adjustFieldLengthIfNeeded() {
109    if (adjustFieldLength.get()) {
110      adjustFieldLength.set(false);
111
112      for (Field f : topScreenModel.getFields()) {
113        if (f.isAutoAdjust()) {
114          int maxLength = topScreenModel.getRecords().stream()
115            .map(r -> r.get(f).asString().length())
116            .max(Integer::compareTo).orElse(0);
117          fieldLengthMap.put(f, Math.max(maxLength, f.getHeader().length()));
118        }
119      }
120    }
121  }
122
123  private List<Header> getDisplayedHeaders() {
124    List<Field> displayFields =
125      topScreenModel.getFields().stream()
126        .filter(fieldDisplayMap::get).collect(Collectors.toList());
127
128    if (displayFields.isEmpty()) {
129      horizontalScroll = 0;
130    } else if (horizontalScroll > displayFields.size() - 1) {
131      horizontalScroll = displayFields.size() - 1;
132    }
133
134    List<Header> ret = new ArrayList<>();
135
136    int length = 0;
137    for (int i = horizontalScroll; i < displayFields.size(); i++) {
138      Field field = displayFields.get(i);
139      int fieldLength = fieldLengthMap.get(field);
140
141      length += fieldLength + 1;
142      if (length > terminalLength) {
143        break;
144      }
145      ret.add(new Header(field, fieldLength));
146    }
147
148    return ret;
149  }
150
151  private List<Record> getDisplayedRecords() {
152    List<Record> ret = new ArrayList<>();
153    for (int i = paging.getPageStartPosition(); i < paging.getPageEndPosition(); i++) {
154      ret.add(topScreenModel.getRecords().get(i));
155    }
156    return ret;
157  }
158
159  private Record getSelectedRecord() {
160    if (topScreenModel.getRecords().isEmpty()) {
161      return null;
162    }
163    return topScreenModel.getRecords().get(paging.getCurrentPosition());
164  }
165
166  public void arrowUp() {
167    paging.arrowUp();
168    refresh(true);
169  }
170
171  public void arrowDown() {
172    paging.arrowDown();
173    refresh(true);
174  }
175
176  public void pageUp() {
177    paging.pageUp();
178    refresh(true);
179  }
180
181  public void pageDown() {
182    paging.pageDown();
183    refresh(true);
184  }
185
186  public void arrowLeft() {
187    if (horizontalScroll > 0) {
188      horizontalScroll -= 1;
189    }
190    refresh(true);
191  }
192
193  public void arrowRight() {
194    if (horizontalScroll < getHeaderSize() - 1) {
195      horizontalScroll += 1;
196    }
197    refresh(true);
198  }
199
200  public void home() {
201    if (horizontalScroll > 0) {
202      horizontalScroll = 0;
203    }
204    refresh(true);
205  }
206
207  public void end() {
208    int headerSize = getHeaderSize();
209    horizontalScroll = headerSize == 0 ? 0 : headerSize - 1;
210    refresh(true);
211  }
212
213  private int getHeaderSize() {
214    return (int) topScreenModel.getFields().stream()
215      .filter(fieldDisplayMap::get).count();
216  }
217
218  public void switchSortOrder() {
219    topScreenModel.switchSortOrder();
220    refresh(true);
221  }
222
223  public ScreenView transitionToHelpScreen(Screen screen, Terminal terminal) {
224    return new HelpScreenView(screen, terminal, refreshDelay.get(), topScreenView);
225  }
226
227  public ScreenView transitionToModeScreen(Screen screen, Terminal terminal) {
228    return new ModeScreenView(screen, terminal, topScreenModel.getCurrentMode(), this::switchMode,
229      topScreenView);
230  }
231
232  public ScreenView transitionToFieldScreen(Screen screen, Terminal terminal) {
233    return new FieldScreenView(screen, terminal,
234      topScreenModel.getCurrentSortField(), topScreenModel.getFields(),
235      fieldDisplayMap,
236      (sortKey, fields, fieldDisplayMap) -> {
237        topScreenModel.setSortFieldAndFields(sortKey, fields);
238        this.fieldDisplayMap.clear();
239        this.fieldDisplayMap.putAll(fieldDisplayMap);
240      }, topScreenView);
241  }
242
243  private void switchMode(Mode nextMode) {
244    topScreenModel.switchMode(nextMode, null, false);
245    reset();
246  }
247
248  public void drillDown() {
249    Record selectedRecord = getSelectedRecord();
250    if (selectedRecord == null) {
251      return;
252    }
253    if (topScreenModel.drillDown(selectedRecord)) {
254      reset();
255      refresh(true);
256    }
257  }
258
259  private void reset() {
260    initFieldDisplayMapAndFieldLengthMap();
261    adjustFieldLength.set(true);
262    paging.init();
263    horizontalScroll = 0;
264    topScreenView.clearTerminal();
265  }
266
267  private void initFieldDisplayMapAndFieldLengthMap() {
268    fieldDisplayMap.clear();
269    fieldLengthMap.clear();
270    for (FieldInfo fieldInfo : topScreenModel.getFieldInfos()) {
271      fieldDisplayMap.put(fieldInfo.getField(), fieldInfo.isDisplayByDefault());
272      fieldLengthMap.put(fieldInfo.getField(), fieldInfo.getDefaultLength());
273    }
274  }
275
276  public ScreenView goToMessageMode(Screen screen, Terminal terminal, int row, String message) {
277    return new MessageModeScreenView(screen, terminal, row, message, topScreenView);
278  }
279
280  public ScreenView goToInputModeForRefreshDelay(Screen screen, Terminal terminal, int row) {
281    return new InputModeScreenView(screen, terminal, row,
282      "Change refresh delay from " + (double) refreshDelay.get() / 1000 + " to", null,
283      (inputString) -> {
284        if (inputString.isEmpty()) {
285          return topScreenView;
286        }
287
288        double delay;
289        try {
290          delay = Double.valueOf(inputString);
291        } catch (NumberFormatException e) {
292          return goToMessageMode(screen, terminal, row, "Unacceptable floating point");
293        }
294
295        refreshDelay.set((long) (delay * 1000));
296        return topScreenView;
297      });
298  }
299
300  public ScreenView goToInputModeForFilter(Screen screen, Terminal terminal, int row,
301    boolean ignoreCase) {
302    return new InputModeScreenView(screen, terminal, row,
303      "add filter #" + (topScreenModel.getFilters().size() + 1) +
304        " (" + (ignoreCase ? "ignoring case" : "case sensitive") + ") as: [!]FLD?VAL",
305      topScreenModel.getFilterHistories(),
306      (inputString) -> {
307        if (inputString.isEmpty()) {
308          return topScreenView;
309        }
310
311        if (!topScreenModel.addFilter(inputString, ignoreCase)) {
312          return goToMessageMode(screen, terminal, row, "Unacceptable filter expression");
313        }
314
315        paging.init();
316        return topScreenView;
317      });
318  }
319
320  public void clearFilters() {
321    topScreenModel.clearFilters();
322    paging.init();
323    refresh(true);
324  }
325
326  public ScreenView goToFilterDisplayMode(Screen screen, Terminal terminal, int row) {
327    return new FilterDisplayModeScreenView(screen, terminal, row, topScreenModel.getFilters(),
328      topScreenView);
329  }
330}