package name.abuchen.portfolio.ui; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Named; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.e4.core.contexts.ContextInjectionFactory; import org.eclipse.e4.core.contexts.EclipseContextFactory; import org.eclipse.e4.core.contexts.IEclipseContext; import org.eclipse.e4.core.di.annotations.Optional; import org.eclipse.e4.core.di.extensions.Preference; import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.e4.ui.di.Focus; import org.eclipse.e4.ui.di.Persist; import org.eclipse.e4.ui.di.UIEventTopic; import org.eclipse.e4.ui.model.application.ui.MDirtyable; import org.eclipse.e4.ui.model.application.ui.basic.MPart; import org.eclipse.e4.ui.services.IServiceConstants; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.ProgressBar; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.ClientFactory; import name.abuchen.portfolio.snapshot.ReportingPeriod; import name.abuchen.portfolio.ui.dialogs.PasswordDialog; import name.abuchen.portfolio.ui.views.ExceptionView; import name.abuchen.portfolio.ui.wizards.client.ClientMigrationDialog; @SuppressWarnings("restriction") public class PortfolioPart implements LoadClientThread.Callback { private abstract class BuildContainerRunnable implements Runnable { @Override public final void run() { if (container != null && !container.isDisposed()) { Composite parent = container.getParent(); parent.setRedraw(false); try { container.dispose(); createContainer(parent); parent.layout(true); } finally { parent.setRedraw(true); } } } public abstract void createContainer(Composite parent); } // compatibility: the value used to be stored in the AbstractHistoricView private static final String REPORTING_PERIODS_KEY = "AbstractHistoricView"; //$NON-NLS-1$ private File clientFile; private Client client; private PreferenceStore preferenceStore = new PreferenceStore(); private List<Job> regularJobs = new ArrayList<>(); private Composite container; private PageBook book; private AbstractFinanceView view; private Control focus; @Inject MDirtyable dirty; @Inject IEclipseContext context; @Inject IEventBroker broker; @Inject @Preference IEclipsePreferences preferences; @PostConstruct public void createComposite(Composite parent, MPart part) throws IOException { // is client available? (e.g. via new file wizard) Client attachedClient = (Client) part.getTransientData().get(Client.class.getName()); if (attachedClient != null) { internalSetClient(attachedClient); dirty.setDirty(true); } // is file name available? (e.g. load file, open on startup) String filename = part.getPersistedState().get(UIConstants.File.PERSISTED_STATE_KEY); if (filename != null) { clientFile = new File(filename); broker.post(UIConstants.Event.File.OPENED, clientFile.getAbsolutePath()); loadPreferences(); } if (attachedClient != null) { createContainerWithViews(parent); } else if (ClientFactory.isEncrypted(clientFile)) { createContainerWithMessage(parent, MessageFormat.format(Messages.MsgOpenFile, clientFile.getName()), false, true); } else { ProgressBar bar = createContainerWithMessage(parent, MessageFormat.format(Messages.MsgLoadingFile, clientFile.getName()), true, false); new LoadClientThread(broker, new ProgressMonitor(bar), this, clientFile, null).start(); } } private void createContainerWithViews(Composite parent) { container = new Composite(parent, SWT.NONE); container.setLayout(new FillLayout()); SashForm sash = new SashForm(container, SWT.HORIZONTAL | SWT.SMOOTH); sash.setSashWidth(3); Composite navigationBar = new Composite(sash, SWT.NONE); GridLayoutFactory.fillDefaults().numColumns(1).spacing(0, 0).margins(0, 0).applyTo(navigationBar); ClientEditorSidebar sidebar = new ClientEditorSidebar(this); Control control = sidebar.createSidebarControl(navigationBar); GridDataFactory.fillDefaults().grab(true, true).applyTo(control); ClientProgressProvider provider = make(ClientProgressProvider.class, client, navigationBar); GridDataFactory.fillDefaults().grab(true, false).applyTo(provider.getControl()); book = new PageBook(sash, SWT.NONE); SashHelper sashHelper = new SashHelper(PortfolioPart.class.getSimpleName() + "-sash", getPreferenceStore()); //$NON-NLS-1$ sashHelper.setConstantWidth(new int[] { 180, -1 }); sashHelper.attachTo(sash); sidebar.selectDefaultView(); focus = book; } /** * Creates window with logo and message. Optional a progress bar (while * loading) or a password input field (if encrypted). */ private ProgressBar createContainerWithMessage(Composite parent, String message, boolean showProgressBar, boolean showPasswordField) { ProgressBar bar = null; container = new Composite(parent, SWT.NONE); container.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); container.setLayout(new FormLayout()); Label image = new Label(container, SWT.NONE); image.setBackground(container.getBackground()); image.setImage(Images.LOGO_48.image()); FormData data = new FormData(); data.top = new FormAttachment(50, -50); data.left = new FormAttachment(50, -24); image.setLayoutData(data); if (showPasswordField) { Text pwd = createPasswordField(container); data = new FormData(); data.top = new FormAttachment(image, 10); data.left = new FormAttachment(image, 0, SWT.CENTER); data.width = 100; pwd.setLayoutData(data); focus = pwd; } else if (showProgressBar) { bar = new ProgressBar(container, SWT.SMOOTH); data = new FormData(); data.top = new FormAttachment(image, 10); data.left = new FormAttachment(50, -100); data.width = 200; bar.setLayoutData(data); } Label label = new Label(container, SWT.CENTER | SWT.WRAP); label.setBackground(container.getBackground()); label.setText(message); data = new FormData(); data.top = new FormAttachment(image, 40); data.left = new FormAttachment(50, -100); data.width = 200; label.setLayoutData(data); return bar; } private Text createPasswordField(Composite container) { final Text pwd = new Text(container, SWT.PASSWORD | SWT.BORDER); pwd.setFocus(); pwd.addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(SelectionEvent e) { final String password = pwd.getText(); Display.getDefault().syncExec(new BuildContainerRunnable() { @Override public void createContainer(Composite parent) { ProgressBar bar = createContainerWithMessage(parent, MessageFormat.format( Messages.MsgLoadingFile, PortfolioPart.this.clientFile.getName()), true, false); new LoadClientThread(broker, new ProgressMonitor(bar), PortfolioPart.this, clientFile, password.toCharArray()).start(); } }); } }); return pwd; } @Override public void setClient(Client client) { // additional safeguard: make a copy of the file that could be // successfully read b/c we get reports of corrupted files if (clientFile != null && preferences.getBoolean(UIConstants.Preferences.CREATE_BACKUP_BEFORE_SAVING, true)) createBackup(clientFile, "backup-after-open"); //$NON-NLS-1$ internalSetClient(client); Display.getDefault().asyncExec(new BuildContainerRunnable() { @Override public void createContainer(Composite parent) { createContainerWithViews(parent); } }); } public void internalSetClient(Client client) { this.client = client; this.dirty.setDirty(false); this.context.set(Client.class, client); client.addPropertyChangeListener(event -> notifyModelUpdated()); if (client.getFileVersionAfterRead() < Client.VERSION_WITH_CURRENCY_SUPPORT) { Display.getDefault().asyncExec(() -> { Dialog dialog = new ClientMigrationDialog(Display.getDefault().getActiveShell(), client); dialog.open(); }); } new ConsistencyChecksJob(client, false).schedule(100); scheduleOnlineUpdateJobs(); } @Override public void setErrorMessage(final String message) { Display.getDefault().asyncExec(new BuildContainerRunnable() { @Override public void createContainer(Composite parent) { createContainerWithMessage(parent, message, false, ClientFactory.isEncrypted(clientFile)); } }); } @Focus public void setFocus() { if (focus != null && !focus.isDisposed()) focus.setFocus(); } @PreDestroy public void destroy() { if (clientFile != null) storePreferences(); regularJobs.forEach(Job::cancel); } @Persist public void save(MPart part, @Named(IServiceConstants.ACTIVE_SHELL) Shell shell) { if (clientFile == null) { doSaveAs(part, shell, null, null); return; } try { part.getPersistedState().put(UIConstants.File.PERSISTED_STATE_KEY, clientFile.getAbsolutePath()); if (preferences.getBoolean(UIConstants.Preferences.CREATE_BACKUP_BEFORE_SAVING, true)) createBackup(clientFile, "backup"); //$NON-NLS-1$ ClientFactory.save(client, clientFile, null, null); broker.post(UIConstants.Event.File.SAVED, clientFile.getAbsolutePath()); dirty.setDirty(false); storePreferences(); } catch (IOException e) { ErrorDialog.openError(shell, Messages.LabelError, e.getMessage(), new Status(Status.ERROR, PortfolioPlugin.PLUGIN_ID, e.getMessage(), e)); } } private void createBackup(File file, String suffix) { try { // keep original extension in order to be able to open the backup // file directly from within PP String filename = file.getName(); int l = filename.lastIndexOf('.'); String backupName = l > 0 ? filename.substring(0, l) + '.' + suffix + filename.substring(l) : filename + '.' + suffix; Path sourceFile = file.toPath(); Path backupFile = sourceFile.resolveSibling(backupName); Files.copy(sourceFile, backupFile, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { PortfolioPlugin.log(e); Display.getDefault().asyncExec(() -> MessageDialog.openError(Display.getDefault().getActiveShell(), Messages.LabelError, e.getMessage())); } } public void doSaveAs(MPart part, Shell shell, String extension, String encryptionMethod) // NOSONAR { FileDialog dialog = new FileDialog(shell, SWT.SAVE); // if an extension is given, make sure the file name proposal has the // right extension in the save as dialog String fileNameProposal = clientFile != null ? clientFile.getName() : part.getLabel(); if (extension != null && !fileNameProposal.endsWith('.' + extension)) { int p = fileNameProposal.lastIndexOf('.'); fileNameProposal = (p > 0 ? fileNameProposal.substring(0, p + 1) : fileNameProposal + '.') + extension; } dialog.setFileName(fileNameProposal); dialog.setFilterPath(clientFile != null ? clientFile.getAbsolutePath() : System.getProperty("user.home")); //$NON-NLS-1$ String path = dialog.open(); if (path == null) return; // again make sure the extension is correct as the user might have // changed it in the save dialog if (extension != null && !path.endsWith('.' + extension)) path += '.' + extension; File localFile = new File(path); char[] password = null; if (ClientFactory.isEncrypted(localFile)) { PasswordDialog pwdDialog = new PasswordDialog(shell); if (pwdDialog.open() != PasswordDialog.OK) return; password = pwdDialog.getPassword().toCharArray(); } try { clientFile = localFile; part.getPersistedState().put(UIConstants.File.PERSISTED_STATE_KEY, clientFile.getAbsolutePath()); ClientFactory.save(client, clientFile, encryptionMethod, password); broker.post(UIConstants.Event.File.SAVED, clientFile.getAbsolutePath()); dirty.setDirty(false); part.setLabel(clientFile.getName()); part.setTooltip(clientFile.getAbsolutePath()); storePreferences(); } catch (IOException e) { PortfolioPlugin.log(e); ErrorDialog.openError(shell, Messages.LabelError, e.getMessage(), new Status(Status.ERROR, PortfolioPlugin.PLUGIN_ID, e.getMessage(), e)); } } public Client getClient() { return client; } public IPreferenceStore getPreferenceStore() { return preferenceStore; } /* package */void markDirty() { dirty.setDirty(true); } @Inject @Optional public void onExchangeRatesLoaded(@UIEventTopic(UIConstants.Event.ExchangeRates.LOADED) Object obj) { // update view w/o marking the model dirty if (view != null && view.getControl() != null && !view.getControl().isDisposed()) view.notifyModelUpdated(); } public void notifyModelUpdated() { Display.getDefault().asyncExec(() -> { markDirty(); if (view != null && view.getControl() != null && !view.getControl().isDisposed()) view.notifyModelUpdated(); }); } @SuppressWarnings("unchecked") public void activateView(String target, Object parameter) { disposeView(); try { Class<?> clazz = getClass().getClassLoader() .loadClass("name.abuchen.portfolio.ui.views." + target + "View"); //$NON-NLS-1$ //$NON-NLS-2$ if (clazz == null) return; createView((Class<AbstractFinanceView>) clazz, parameter); } catch (Exception e) { PortfolioPlugin.log(e); createView(ExceptionView.class, e); } } private void createView(Class<? extends AbstractFinanceView> clazz, Object parameter) { IEclipseContext viewContext = this.context.createChild(); viewContext.set(Client.class, this.client); viewContext.set(IPreferenceStore.class, this.preferenceStore); viewContext.set(PortfolioPart.class, this); view = ContextInjectionFactory.make(clazz, viewContext); viewContext.set(AbstractFinanceView.class, view); view.setContext(viewContext); view.init(this, parameter); view.createViewControl(book); book.showPage(view.getControl()); view.setFocus(); } private void disposeView() { if (view != null) { view.getContext().dispose(); if (!view.getControl().isDisposed()) view.getControl().dispose(); view = null; } } private void scheduleOnlineUpdateJobs() { if (!"no".equals(System.getProperty("name.abuchen.portfolio.auto-updates"))) //$NON-NLS-1$ //$NON-NLS-2$ { new UpdateQuotesJob(client, EnumSet.of(UpdateQuotesJob.Target.LATEST, UpdateQuotesJob.Target.HISTORIC)) .schedule(1000); int tenMinutes = 1000 * 60 * 10; Job job = new UpdateQuotesJob(client, EnumSet.of(UpdateQuotesJob.Target.LATEST)).repeatEvery(tenMinutes); job.schedule(tenMinutes); regularJobs.add(job); int sixHours = 1000 * 60 * 60 * 6; job = new UpdateQuotesJob(client, EnumSet.of(UpdateQuotesJob.Target.HISTORIC)).repeatEvery(sixHours); job.schedule(sixHours); regularJobs.add(job); new UpdateCPIJob(client).schedule(1000); } } // ////////////////////////////////////////////////////////////// // preference store functions // ////////////////////////////////////////////////////////////// public LinkedList<ReportingPeriod> loadReportingPeriods() // NOSONAR { LinkedList<ReportingPeriod> answer = new LinkedList<>(); String config = getPreferenceStore().getString(REPORTING_PERIODS_KEY); if (config != null && config.trim().length() > 0) { String[] codes = config.split(";"); //$NON-NLS-1$ for (String c : codes) { try { answer.add(ReportingPeriod.from(c)); } catch (IOException | RuntimeException ignore) { PortfolioPlugin.log(ignore); } } } if (answer.isEmpty()) { for (int ii = 1; ii <= 5; ii++) answer.add(new ReportingPeriod.LastX(ii, 0)); } return answer; } public void storeReportingPeriods(List<ReportingPeriod> periods) { StringBuilder buf = new StringBuilder(); for (ReportingPeriod p : periods) { p.writeTo(buf); buf.append(';'); } getPreferenceStore().setValue(REPORTING_PERIODS_KEY, buf.toString()); } private void storePreferences() { if (clientFile != null && preferenceStore.needsSaving()) { try { preferenceStore.setFilename(getPreferenceStoreFile(clientFile).getAbsolutePath()); preferenceStore.save(); } catch (IOException ignore) { PortfolioPlugin.log(ignore); } } } private void loadPreferences() { if (clientFile != null) { try { File preferenceFile = getPreferenceStoreFile(clientFile); preferenceStore.setFilename(preferenceFile.getAbsolutePath()); if (preferenceFile.exists()) { preferenceStore.load(); } } catch (IOException ignore) { PortfolioPlugin.log(ignore); } } } private File getPreferenceStoreFile(File file) throws IOException { try { byte[] digest = MessageDigest.getInstance("MD5").digest(file.getAbsolutePath().getBytes()); //$NON-NLS-1$ StringBuilder filename = new StringBuilder(); filename.append("prf_"); //$NON-NLS-1$ for (int i = 0; i < digest.length; i++) filename.append(Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1)); filename.append(".txt"); //$NON-NLS-1$ return new File(PortfolioPlugin.getDefault().getStateLocation().toFile(), filename.toString()); } catch (NoSuchAlgorithmException e) { throw new IOException(e); } } private <T> T make(Class<T> type, Object... parameters) { IEclipseContext c2 = EclipseContextFactory.create(); if (parameters != null) for (Object param : parameters) c2.set(param.getClass().getName(), param); return ContextInjectionFactory.make(type, this.context, c2); } }