Project

General

Profile

Download (20.9 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2007 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9

    
10
package eu.etaxonomy.taxeditor.bulkeditor.e4;
11

    
12
import java.io.Serializable;
13
import java.util.ArrayList;
14
import java.util.HashMap;
15
import java.util.List;
16
import java.util.Map;
17

    
18
import javax.annotation.PostConstruct;
19
import javax.annotation.PreDestroy;
20
import javax.inject.Inject;
21

    
22
import org.eclipse.core.runtime.IProgressMonitor;
23
import org.eclipse.core.runtime.NullProgressMonitor;
24
import org.eclipse.e4.core.di.annotations.Optional;
25
import org.eclipse.e4.core.services.events.IEventBroker;
26
import org.eclipse.e4.ui.di.Focus;
27
import org.eclipse.e4.ui.di.Persist;
28
import org.eclipse.e4.ui.di.UIEventTopic;
29
import org.eclipse.e4.ui.model.application.ui.MDirtyable;
30
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
31
import org.eclipse.e4.ui.services.EMenuService;
32
import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
33
import org.eclipse.jface.dialogs.MessageDialog;
34
import org.eclipse.jface.layout.GridDataFactory;
35
import org.eclipse.jface.viewers.ISelectionChangedListener;
36
import org.eclipse.jface.viewers.IStructuredSelection;
37
import org.eclipse.jface.viewers.StructuredSelection;
38
import org.eclipse.nebula.widgets.nattable.NatTable;
39
import org.eclipse.nebula.widgets.nattable.command.VisualRefreshCommand;
40
import org.eclipse.nebula.widgets.nattable.command.VisualRefreshCommandHandler;
41
import org.eclipse.nebula.widgets.nattable.config.AbstractRegistryConfiguration;
42
import org.eclipse.nebula.widgets.nattable.config.AbstractUiBindingConfiguration;
43
import org.eclipse.nebula.widgets.nattable.config.ConfigRegistry;
44
import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
45
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
46
import org.eclipse.nebula.widgets.nattable.config.IEditableRule;
47
import org.eclipse.nebula.widgets.nattable.data.IRowIdAccessor;
48
import org.eclipse.nebula.widgets.nattable.data.ListDataProvider;
49
import org.eclipse.nebula.widgets.nattable.edit.EditConfigAttributes;
50
import org.eclipse.nebula.widgets.nattable.extension.glazedlists.GlazedListsEventLayer;
51
import org.eclipse.nebula.widgets.nattable.extension.glazedlists.GlazedListsSortModel;
52
import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
53
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultColumnHeaderDataProvider;
54
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
55
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultRowHeaderDataProvider;
56
import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer;
57
import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
58
import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
59
import org.eclipse.nebula.widgets.nattable.grid.layer.RowHeaderLayer;
60
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
61
import org.eclipse.nebula.widgets.nattable.layer.stack.DefaultBodyLayerStack;
62
import org.eclipse.nebula.widgets.nattable.resize.command.InitializeAutoResizeColumnsCommand;
63
import org.eclipse.nebula.widgets.nattable.selection.RowSelectionModel;
64
import org.eclipse.nebula.widgets.nattable.selection.RowSelectionProvider;
65
import org.eclipse.nebula.widgets.nattable.sort.SortHeaderLayer;
66
import org.eclipse.nebula.widgets.nattable.sort.config.SingleClickSortConfiguration;
67
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
68
import org.eclipse.nebula.widgets.nattable.style.theme.ModernNatTableThemeConfiguration;
69
import org.eclipse.nebula.widgets.nattable.ui.binding.UiBindingRegistry;
70
import org.eclipse.nebula.widgets.nattable.ui.matcher.MouseEventMatcher;
71
import org.eclipse.nebula.widgets.nattable.ui.menu.PopupMenuAction;
72
import org.eclipse.nebula.widgets.nattable.ui.menu.PopupMenuBuilder;
73
import org.eclipse.nebula.widgets.nattable.util.GCFactory;
74
import org.eclipse.swt.SWT;
75
import org.eclipse.swt.dnd.Clipboard;
76
import org.eclipse.swt.dnd.TextTransfer;
77
import org.eclipse.swt.dnd.Transfer;
78
import org.eclipse.swt.layout.GridData;
79
import org.eclipse.swt.layout.GridLayout;
80
import org.eclipse.swt.widgets.Composite;
81
import org.eclipse.swt.widgets.Display;
82
import org.eclipse.swt.widgets.Menu;
83

    
84
import ca.odell.glazedlists.SortedList;
85
import eu.etaxonomy.cdm.api.conversation.ConversationHolder;
86
import eu.etaxonomy.cdm.api.conversation.IConversationEnabled;
87
import eu.etaxonomy.cdm.api.facade.DerivedUnitFacade;
88
import eu.etaxonomy.cdm.model.common.CdmBase;
89
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
90
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
91
import eu.etaxonomy.cdm.model.occurrence.FieldUnit;
92
import eu.etaxonomy.cdm.model.taxon.Taxon;
93
import eu.etaxonomy.cdm.persistence.hibernate.CdmDataChangeMap;
94
import eu.etaxonomy.taxeditor.bulkeditor.BulkEditorQuery;
95
import eu.etaxonomy.taxeditor.bulkeditor.input.AbstractBulkEditorInput;
96
import eu.etaxonomy.taxeditor.bulkeditor.input.sortprovider.TitleCacheComparator;
97
import eu.etaxonomy.taxeditor.editor.ITaxonEditor;
98
import eu.etaxonomy.taxeditor.event.WorkbenchEventConstants;
99
import eu.etaxonomy.taxeditor.l10n.Messages;
100
import eu.etaxonomy.taxeditor.model.IDerivedUnitFacadePart;
101
import eu.etaxonomy.taxeditor.model.IDirtyMarkable;
102
import eu.etaxonomy.taxeditor.model.IPartContentHasDetails;
103
import eu.etaxonomy.taxeditor.model.IPartContentHasFactualData;
104
import eu.etaxonomy.taxeditor.model.IPartContentHasMedia;
105
import eu.etaxonomy.taxeditor.model.IPartContentHasSupplementalData;
106
import eu.etaxonomy.taxeditor.operation.IPostOperationEnabled;
107
import eu.etaxonomy.taxeditor.workbench.part.IE4SavablePart;
108

    
109
/**
110
 *
111
 * @author pplitzner
112
 * @since Sep 8, 2017
113
 *
114
 */
115
public class BulkEditorE4 implements IPartContentHasDetails, IConversationEnabled, IPostOperationEnabled,
116
        IDirtyMarkable, IDerivedUnitFacadePart, IPartContentHasFactualData,
117
        IPartContentHasSupplementalData, IPartContentHasMedia, IE4SavablePart, ITaxonEditor {
118

    
119
    public static final String TYPE_PROPERTY = "Type";
120

    
121
    @Inject
122
	private MDirtyable dirty;
123

    
124
    private AbstractBulkEditorInput input;
125

    
126
    private Composite topComposite;
127

    
128
    private ConversationHolder conversation;
129

    
130
    @Inject
131
    private ESelectionService selService;
132

    
133
    @Inject
134
    private IEventBroker eventBroker;
135

    
136
    private ISelectionChangedListener selectionChangedListener;
137

    
138
    @Inject
139
    private MPart thisPart;
140

    
141
    private BulkEditorQuery lastQuery = null;
142

    
143
    private Composite bottomComposite;
144

    
145
    @Inject
146
    private EMenuService menuService;
147

    
148
    private NatTable natTable;
149

    
150
    private DefaultBodyLayerStack bodyLayer;
151

    
152
    private ListDataProvider<CdmBase> bodyDataProvider;
153

    
154
    @Inject
155
    public BulkEditorE4() {
156
	}
157

    
158
	@SuppressWarnings("unused")
159
    public void init(AbstractBulkEditorInput<?> input){
160
	    this.input = input;
161
	    this.conversation = input.getConversation();
162

    
163
	    new BulkEditorSearchE4(this, topComposite, SWT.NONE);
164
	    //layout needed because the search bar is added after @PostConstuct method
165
	    topComposite.getParent().layout();
166

    
167
	    thisPart.setLabel(input.getEditorName());
168

    
169
        if(input.getEntityUuid()!=null){
170
            performSearch(new BulkEditorQuery(input.getEntityUuid().toString()));
171
        }
172

    
173
        createTable();
174

    
175
        configureTable();
176

    
177
        styleTable();
178

    
179
        GridDataFactory.fillDefaults().grab(true, true).applyTo(natTable);
180

    
181
        //propagate selection
182
        selectionChangedListener = (event -> selService.setSelection(getSelection()));
183
        RowSelectionProvider<CdmBase> selectionProvider = new RowSelectionProvider<CdmBase>(bodyLayer.getSelectionLayer(), bodyDataProvider, true);
184
        selectionProvider.addSelectionChangedListener(selectionChangedListener);
185

    
186
        bottomComposite.layout();
187
	}
188

    
189
	private void createTable(){
190
	    ConfigRegistry configRegistry = new ConfigRegistry();
191
	    //property map
192
        Map<String, String> propertyToLabels = new HashMap<>();
193
        propertyToLabels.put(getEditorInput().getName(), getEditorInput().getName());
194
        propertyToLabels.put(TYPE_PROPERTY, TYPE_PROPERTY);
195
        String[] propertyNames = new String[] { input.getName(), TYPE_PROPERTY };
196
        //sorted list
197
        SortedList<CdmBase> sortedList = new SortedList<>(input.getModel(), new TitleCacheComparator());
198
        //data provider
199
        BulkEditorPropertyAccessor columnPropertyAccessor = new BulkEditorPropertyAccessor(input);
200
        bodyDataProvider = new ListDataProvider<CdmBase>(sortedList,
201
                columnPropertyAccessor);
202
        DefaultColumnHeaderDataProvider colHeaderDataProvider = new DefaultColumnHeaderDataProvider(
203
                propertyNames, propertyToLabels);
204
        DefaultRowHeaderDataProvider rowHeaderDataProvider = new DefaultRowHeaderDataProvider(
205
                bodyDataProvider);
206
        //body
207
        DataLayer dataLayer = new DataLayer(bodyDataProvider);
208
        dataLayer.registerCommandHandler(new VisualRefreshCommandHandler());
209
        GlazedListsEventLayer<CdmBase> eventLayer = new GlazedListsEventLayer<>(dataLayer, input.getModel());
210
        bodyLayer = new DefaultBodyLayerStack(eventLayer);
211
        //column
212
        DataLayer columnHeaderDataLayer = new DataLayer(colHeaderDataProvider);
213
        ColumnHeaderLayer columnHeaderLayer = new ColumnHeaderLayer(
214
                columnHeaderDataLayer,
215
                bodyLayer, bodyLayer.getSelectionLayer());
216
        // add the SortHeaderLayer to the column header layer stack
217
        // as we use GlazedLists, we use the GlazedListsSortModel which
218
        // delegates the sorting to the SortedList
219
        final SortHeaderLayer<SpecimenDescription> sortHeaderLayer = new SortHeaderLayer<>(
220
                columnHeaderLayer,
221
                new GlazedListsSortModel<>(
222
                        sortedList,
223
                        columnPropertyAccessor,
224
                        configRegistry,
225
                        columnHeaderDataLayer));
226
        //row
227
        DataLayer rowHeaderDataLayer = new DataLayer(rowHeaderDataProvider);
228
        RowHeaderLayer rowHeaderLayer = new RowHeaderLayer(rowHeaderDataLayer,
229
                bodyLayer, bodyLayer.getSelectionLayer());
230
        rowHeaderDataLayer.setDefaultRowHeight(20);
231
        rowHeaderDataLayer.setColumnWidthByPosition(0, 30);
232
        rowHeaderDataLayer.setColumnWidthByPosition(1, 200);
233
        rowHeaderDataLayer.setColumnWidthByPosition(2, 100);
234

    
235
        //corner
236
        DefaultCornerDataProvider cornerDataProvider = new DefaultCornerDataProvider(
237
                colHeaderDataProvider, rowHeaderDataProvider);
238
        CornerLayer cornerLayer = new CornerLayer(new DataLayer(
239
                cornerDataProvider), rowHeaderLayer, sortHeaderLayer);
240
        //grid
241
        GridLayer gridLayer = new GridLayer(bodyLayer, sortHeaderLayer,
242
                rowHeaderLayer, cornerLayer);
243

    
244
        natTable = new NatTable(bottomComposite, gridLayer, false);
245
        natTable.setConfigRegistry(configRegistry);
246

    
247
        //full row selection
248
        bodyLayer.getSelectionLayer().setSelectionModel(new RowSelectionModel<CdmBase>(bodyLayer.getSelectionLayer(), bodyDataProvider, new IRowIdAccessor<CdmBase>() {
249
            @Override
250
            public Serializable getRowId(CdmBase rowObject) {
251
                return input.getModel().indexOf(rowObject);
252
            }
253
        }));
254
	}
255

    
256
	private void configureTable(){
257
        //+++CONTEXT MENU+++
258
        menuService.registerContextMenu(natTable, "eu.etaxonomy.taxeditor.bulkeditor.popupmenu.bulkeditor"); //$NON-NLS-1$
259
        // get the menu registered by EMenuService
260
        final Menu e4Menu = natTable.getMenu();
261
        // remove the menu reference from NatTable instance
262
        natTable.setMenu(null);
263
        natTable.addConfiguration(
264
                new AbstractUiBindingConfiguration() {
265
            @Override
266
            public void configureUiBindings(
267
                    UiBindingRegistry uiBindingRegistry) {
268
                // add e4 menu to NatTable
269
                new PopupMenuBuilder(natTable, e4Menu)
270
                    .build();
271

    
272
                // register the UI binding for header, corner and body region
273
                uiBindingRegistry.registerMouseDownBinding(
274
                        new MouseEventMatcher(
275
                                SWT.NONE,
276
                                GridRegion.BODY,
277
                                MouseEventMatcher.RIGHT_BUTTON),
278
                        new PopupMenuAction(e4Menu));
279
                uiBindingRegistry.registerMouseDownBinding(
280
                        new MouseEventMatcher(
281
                                SWT.NONE,
282
                                GridRegion.COLUMN_HEADER,
283
                                MouseEventMatcher.RIGHT_BUTTON),
284
                        new PopupMenuAction(e4Menu));
285
                uiBindingRegistry.registerMouseDownBinding(
286
                        new MouseEventMatcher(
287
                                SWT.NONE,
288
                                GridRegion.CORNER,
289
                                MouseEventMatcher.RIGHT_BUTTON),
290
                        new PopupMenuAction(e4Menu));
291
            }
292
        });
293

    
294
        //make cells editable to allow selecting the text
295
        natTable.addConfiguration(new AbstractRegistryConfiguration() {
296
            @Override
297
            public void configureRegistry(IConfigRegistry configRegistry) {
298
              //make cell editable
299
                configRegistry.registerConfigAttribute(
300
                        EditConfigAttributes.CELL_EDITABLE_RULE,
301
                        IEditableRule.ALWAYS_EDITABLE,
302
                        DisplayMode.EDIT);
303
            }
304
        });
305

    
306
        //enable sorting
307
        natTable.addConfiguration(new SingleClickSortConfiguration());
308
        //add default configuration because autoconfigure is set to false in constructor
309
        natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
310
        natTable.configure();
311
	}
312

    
313
	private void styleTable(){
314
	    natTable.setTheme(new ModernNatTableThemeConfiguration());
315
	}
316

    
317
	/** {@inheritDoc} */
318
	@PostConstruct
319
	public void createPartControl(Composite parent) {
320
		parent.setLayout(new GridLayout());
321

    
322
		topComposite = new Composite(parent, SWT.NONE);
323
		topComposite.setLayout(new GridLayout());
324

    
325
		GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false);
326
		topComposite.setLayoutData(gridData);
327

    
328
		bottomComposite = new Composite(parent, SWT.NONE);
329
		bottomComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
330
		bottomComposite.setLayout(new GridLayout());
331
	}
332

    
333
	@Override
334
	@Persist
335
	public void save(IProgressMonitor monitor) {
336
	    save(monitor, true);
337
	}
338

    
339
	@Focus
340
	public void setFocus() {
341
        //make sure to bind again if maybe in another view the conversation was unbound
342
        if(conversation!=null && !conversation.isBound()){
343
            conversation.bind();
344
        }
345
        if(input!=null && input.getCdmEntitySession()!= null) {
346
            input.getCdmEntitySession().bind();
347
        }
348

    
349
	    //make sure to bind again if maybe in another view the conversation was unbound
350
	    eventBroker.post(WorkbenchEventConstants.CURRENT_ACTIVE_EDITOR, this);
351
	}
352

    
353
	@PreDestroy
354
	public void dispose() {
355
	    if(conversation!=null){
356
	        conversation.unregisterForDataStoreChanges(this);
357
	        conversation.close();
358
	    }
359
	    if(input!=null){
360
	        input.dispose();
361
	    }
362
	    dirty.setDirty(false);
363
	}
364

    
365
	public void save(IProgressMonitor monitor, boolean resetMerge) {
366
	    if (!input.getCdmEntitySession().isActive()){
367
            input.getCdmEntitySession().bind();
368
        }
369
	    input.saveModel(resetMerge);
370

    
371
	    IStructuredSelection selection = getSelection();
372

    
373
        dirty.setDirty(false);
374
        input.dispose();
375
        input.bind();
376
        conversation.commit(true);
377

    
378
        if (lastQuery != null){
379
            performSearch(lastQuery, selection);
380
        }
381
	}
382

    
383
	public void performSearch(BulkEditorQuery query) {
384
	    performSearch(query, null);
385
	}
386

    
387
	/** {@inheritDoc}
388
	 * @param selection */
389
    public void performSearch(BulkEditorQuery query, IStructuredSelection selection) {
390
        if (query != null) {
391
            // TODO check if dirty, prompt save
392
            if (isDirty()) {
393
                String[] labels = {Messages.BulkEditorE4_SAVE_AND_SEARCH, Messages.BulkEditorE4_DONT_SAVE,Messages.BulkEditorE4_CANCEL};
394
                MessageDialog dialog =new MessageDialog(topComposite.getShell(), Messages.BulkEditorE4_SAVE_CHANGES_TITLE, null, Messages.BulkEditorE4_SAVE_CHANGES_MESSAGE, MessageDialog.QUESTION,labels, 0);
395
                int result = dialog.open();
396
                if (result == 0) {
397
                    save(new NullProgressMonitor());
398
                } else if (result == 2){
399
                    return;
400
                }
401
            }
402
            dirty.setDirty(false);
403
            input.performSearch(query, selection);
404
            lastQuery = query;
405
        }
406
    }
407

    
408
    @Optional
409
    @Inject
410
    private void updateAfterSearch(@UIEventTopic(WorkbenchEventConstants.BULK_EDITOR_SEARCH_FINISHED)IStructuredSelection selection){
411
        if(selection!=null){
412
            setSelection(selection);
413
        }
414
        //auto resize columns
415
        InitializeAutoResizeColumnsCommand command = new InitializeAutoResizeColumnsCommand(
416
                natTable, 2, natTable.getConfigRegistry(), new GCFactory(
417
                        natTable));
418
        natTable.doCommand(command);
419
        command = new InitializeAutoResizeColumnsCommand(
420
                natTable, 1, natTable.getConfigRegistry(), new GCFactory(
421
                        natTable));
422
        natTable.doCommand(command);
423
    }
424

    
425
    public void refresh(){
426
        natTable.doCommand(new VisualRefreshCommand());
427
    }
428

    
429
    public IStructuredSelection getSelection(){
430
        List<CdmBase> selection = new ArrayList<>();
431
        int[] fullySelectedRowPositions = bodyLayer.getSelectionLayer().getFullySelectedRowPositions();
432
        for (int i : fullySelectedRowPositions) {
433
            Object rowObject = bodyDataProvider.getRowObject(i);
434
            if(rowObject instanceof CdmBase){
435
                selection.add((CdmBase) rowObject);
436
            }
437
        }
438
        return new StructuredSelection(selection);
439
    }
440

    
441
    public void setSelection(IStructuredSelection selection){
442
        Object[] objects = selection.toArray();
443
        for (Object object : objects) {
444
            if(object instanceof CdmBase){
445
                bodyLayer.getSelectionLayer().selectRow(0, bodyDataProvider.indexOfRowObject((CdmBase) object), false, false);
446
            }
447
        }
448
    }
449

    
450
    public void setDirty(){
451
        dirty.setDirty(true);
452
    }
453

    
454
    public boolean isDirty() {
455
        return dirty.isDirty();
456
    }
457

    
458
    public AbstractBulkEditorInput getEditorInput() {
459
        return input;
460
    }
461

    
462
    public void copyDataToClipboard() {
463
        String textData = "";
464
        IStructuredSelection selection = getSelection();
465
        Object[] objects = selection.toArray();
466
        for (Object object : objects) {
467
            if(object instanceof CdmBase){
468
                textData += getEditorInput().getText((CdmBase)object);
469
            }
470
        }
471
        final TextTransfer textTransfer = TextTransfer.getInstance();
472
        final Clipboard clipboard = new Clipboard(Display.getDefault());
473
        try {
474
            clipboard.setContents(new Object[] { textData.toString() },
475
                    new Transfer[] { textTransfer });
476
        } finally {
477
            clipboard.dispose();
478
        }
479
    }
480

    
481
    @Override
482
    public void update(CdmDataChangeMap arg0) {
483
    }
484

    
485
    @Override
486
    public boolean canAttachMedia() {
487
        return true;
488
    }
489

    
490
    @Override
491
    public void changed(Object element) {
492
        if(element instanceof DerivedUnitFacade){
493
            DerivedUnit derivedUnit = ((DerivedUnitFacade) element).innerDerivedUnit();
494
            if(derivedUnit!=null){
495
                getEditorInput().addSaveCandidate(derivedUnit);
496
            }
497
            FieldUnit fieldUnit = ((DerivedUnitFacade) element).innerFieldUnit();
498
            if(fieldUnit!=null){
499
                getEditorInput().addSaveCandidate(fieldUnit);
500
            }
501
        }
502
        else if (element instanceof CdmBase) {
503
            getEditorInput().addSaveCandidate((CdmBase)element);
504
            input.replaceInModel((CdmBase) element);
505
        }
506
        dirty.setDirty(true);
507
        setSelection(new StructuredSelection(element));
508
    }
509

    
510
    @Override
511
    public void forceDirty() {
512
        dirty.setDirty(true);
513
    }
514

    
515
    @Override
516
    public boolean postOperation(CdmBase objectAffectedByOperation) {
517
        return false;
518
    }
519

    
520
    @Override
521
    public boolean onComplete() {
522
        return false;
523
    }
524

    
525
    @Override
526
    public ConversationHolder getConversationHolder() {
527
        return conversation;
528
    }
529

    
530
    public BulkEditorQuery getLastQuery() {
531
       return lastQuery;
532
    }
533

    
534
    public void setLastQuery(BulkEditorQuery lastQuery) {
535
       this.lastQuery = lastQuery;
536

    
537
    }
538

    
539
    @Override
540
    public Taxon getTaxon() {
541
        IStructuredSelection selection = getSelection();
542
        if(selection.size()==1) {
543
            Object object = selection.iterator().next();
544
            if(object instanceof Taxon){
545
                return (Taxon) object;
546
            }
547
        }
548
        return null;
549
    }
550

    
551
}
(1-1/5)