package org.gbif.ipt.action.admin; import org.gbif.ipt.action.POSTAction; import org.gbif.ipt.config.AppConfig; import org.gbif.ipt.config.ConfigWarnings; import org.gbif.ipt.model.Extension; import org.gbif.ipt.model.Vocabulary; import org.gbif.ipt.service.DeletionNotAllowedException; import org.gbif.ipt.service.InvalidConfigException; import org.gbif.ipt.service.RegistryException; import org.gbif.ipt.service.admin.ExtensionManager; import org.gbif.ipt.service.admin.RegistrationManager; import org.gbif.ipt.service.admin.VocabulariesManager; import org.gbif.ipt.service.registry.RegistryManager; import org.gbif.ipt.struts2.SimpleTextProvider; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.collect.Ordering; import com.google.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; /** * The Action responsible for all user input relating to extension management. */ public class ExtensionsAction extends POSTAction { // logging private static final Logger LOG = Logger.getLogger(ExtensionsAction.class); private final ExtensionManager extensionManager; private final VocabulariesManager vocabManager; private final RegistryManager registryManager; // list of latest registered extension versions private List<Extension> latestExtensionVersions; private List<Extension> extensions; private Extension extension; private String url; private Boolean synchronise = false; private Date lastSynchronised; private List<Extension> newExtensions; private ConfigWarnings warnings; private boolean upToDate = true; @Inject public ExtensionsAction(SimpleTextProvider textProvider, AppConfig cfg, RegistrationManager registrationManager, ExtensionManager extensionManager, VocabulariesManager vocabManager, RegistryManager registryManager, ConfigWarnings warnings) { super(textProvider, cfg, registrationManager); this.extensionManager = extensionManager; this.vocabManager = vocabManager; this.registryManager = registryManager; this.warnings = warnings; } @Override public String delete() throws Exception { try { extensionManager.uninstallSafely(id); addActionMessage(getText("admin.extension.delete.success", new String[] {id})); } catch (DeletionNotAllowedException e) { addActionWarning(getText("admin.extension.delete.error", new String[] {id})); addActionExceptionWarning(e); } return SUCCESS; } /** * Update installed extension to latest version. * </br> * This involves migrating all associated resource mappings over to the new version. * </br> * If there are no associated resource mappings, the new version can simply be installed. * * @return struts2 result */ public String update() throws Exception { try { LOG.info("Updating extension " + id + " to latest version..."); extensionManager.update(id); addActionMessage(getText("admin.extension.update.success", new String[] {id})); } catch (Exception e) { LOG.error(e); addActionWarning(getText("admin.extension.update.error", new String[] {e.getMessage()}), e); } return SUCCESS; } public Extension getExtension() { return extension; } public List<Extension> getExtensions() { return extensions; } public List<Extension> getNewExtensions() { return newExtensions; } /** * Handles the population of installed and uninstalled extensions on the "Core Types and Extensions" page. * This method always tries to pick up newly registered extensions from the Registry. * </br> * Optionally, the user may have triggered synchronise action, which updates default vocabularies to use latest * versions, and synchronises all installed extensions and vocabularies with the registry to ensure their content * is up-to-date. * * @return struts2 result */ public String list() { if (synchronise) { try { synchronise(); addActionMessage(getText("admin.extensions.synchronise.success")); } catch (Exception e) { String errorMsg = e.getMessage(); if (e instanceof RegistryException) { errorMsg = RegistryException.logRegistryException(((RegistryException)e).getType(), this); } addActionWarning(getText("admin.extensions.synchronise.error", new String[] {errorMsg})); LOG.error(e); } } // retrieve all extensions that have been installed already extensions = extensionManager.list(); // update each installed extension indicating whether it is the latest version (for its rowType) or not updateIsLatest(extensions); // populate list of uninstalled extensions, removing extensions installed already, showing only latest versions newExtensions = getLatestExtensionVersions(); for (Extension e : extensions) { newExtensions.remove(e); } // find date extensions were last synchronised for (Extension ex : extensions) { if (lastSynchronised == null || lastSynchronised.before(ex.getModified())) { lastSynchronised = ex.getModified(); } } return SUCCESS; } @Override public void prepare() { super.prepare(); // load latest extension versions from Registry loadLatestExtensionVersions(); // ensure mandatory vocabs are always loaded vocabManager.load(); if (id != null) { extension = extensionManager.get(id); if (extension == null) { // set notFound flag to true so POSTAction will return a NOT_FOUND 404 result name notFound = true; } } } /** * Method used for 1) updating each extensions' isLatest field, and 2) for action logging (logging if at least * one extension is not up-to-date). * </br> * Works by iterating through list of installed extensions. Updates each one, indicating if it is the latest version * or not. Plus, updates boolean "upToDate", set to false if there is at least one extension that is not up-to-date. */ @VisibleForTesting protected void updateIsLatest(List<Extension> extensions) { if (!extensions.isEmpty()) { try { // complete list of registered extensions (latest and non-latest versions) List<Extension> registered = registryManager.getExtensions(); for (Extension extension : extensions) { extension.setLatest(true); for (Extension rExtension : registered) { // check if registered extension is latest, and if it is, try to use it in comparison if (rExtension.isLatest() && extension.getRowType().equalsIgnoreCase(rExtension.getRowType())) { Date issuedOne = extension.getIssued(); Date issuedTwo = rExtension.getIssued(); if (issuedOne == null && issuedTwo != null) { setUpToDate(false); extension.setLatest(false); LOG.debug("Installed extension with rowType " + extension.getRowType() + " has no issued date. A newer version issued " + issuedTwo.toString() + " exists."); } else if (issuedTwo != null && issuedTwo.compareTo(issuedOne) > 0) { setUpToDate(false); extension.setLatest(false); LOG.debug("Installed extension with rowType " + extension.getRowType() + " was issued " + issuedOne.toString() + ". A newer version issued " + issuedTwo.toString() + " exists."); } else { LOG.debug("Installed extension with rowType " + extension.getRowType() + " is the latest version"); } break; } } } // warn user if updates to installed extensions are available if (isUpToDate()) { addActionMessage(getText("admin.extensions.upToDate")); } else { addActionWarning(getText("admin.extensions.not.upToDate")); } } catch (RegistryException e) { // add startup error message about Registry error String msg = RegistryException.logRegistryException(e.getType(), this); warnings.addStartupError(msg); LOG.error(msg); // add startup error message that explains the consequence of the Registry error msg = getText("admin.extensions.couldnt.load", new String[] {cfg.getRegistryUrl()}); warnings.addStartupError(msg); LOG.error(msg); } } } /** * Reload the list of registered extensions, loading only the latest extension versions. */ private void loadLatestExtensionVersions() { try { // list of all registered extensions List<Extension> all = registryManager.getExtensions(); if (!all.isEmpty()) { // list of latest extension versions setLatestExtensionVersions(getLatestVersions(all)); } } catch (RegistryException e) { // add startup error message that explains why the Registry error occurred String msg = RegistryException.logRegistryException(e.getType(), this); warnings.addStartupError(msg); LOG.error(msg); // add startup error message that explains the consequence of the Registry error msg = getText("admin.extensions.couldnt.load", new String[] {cfg.getRegistryUrl()}); warnings.addStartupError(msg); LOG.error(msg); } finally { // initialize list as empty list if the list could not be populated if (getLatestExtensionVersions() == null) { setLatestExtensionVersions(new ArrayList<Extension>()); } } } /** * Filter a list of extensions, returning the latest version for each rowType. The latest version of an extension * is determined by its issued date. * * @param extensions unfiltered list of all registered extensions * * @return filtered list of extensions */ @VisibleForTesting protected List<Extension> getLatestVersions(List<Extension> extensions) { Ordering<Extension> byIssuedDate = Ordering.natural().nullsFirst().onResultOf(new Function<Extension, Date>() { public Date apply(Extension extension) { return extension.getIssued(); } }); // sort extensions by issued date, starting with latest issued List<Extension> sorted = byIssuedDate.immutableSortedCopy(extensions).reverse(); // populate list of latest extension versions Map<String, Extension> extensionsByRowtype = new HashMap<String, Extension>(); if (!sorted.isEmpty()) { for (Extension extension : sorted) { String rowType = extension.getRowType(); if (rowType != null && !extensionsByRowtype.containsKey(rowType)) { extensionsByRowtype.put(rowType, extension); } } } return new ArrayList<Extension>(extensionsByRowtype.values()); } @Override public String save() { try { extensionManager.install(new URL(url)); addActionMessage(getText("admin.extension.install.success", new String[] {url})); } catch (Exception e) { LOG.error(e); addActionWarning(getText("admin.extension.install.error", new String[] {url}), e); } return SUCCESS; } /** * Ensures the default installed vocabularies always use the latest version. * </br> * Then synchronises all installed extensions and vocabularies with registry to make sure their content is * up-to-date. * * @throws IOException if an extension or vocabulary file cannot be downloaded * @throws RegistryException if the list of registered extensions or vocabularies cannot be loaded from Registry * @throws InvalidConfigException if any of the extensions or vocabularies synchronised is invalid (e.g. bad URL) */ private void synchronise() throws IOException, RegistryException, InvalidConfigException { LOG.info("Update default vocabularies to use latest versions..."); vocabManager.installOrUpdateDefaults(); LOG.info("Updating content of all installed vocabularies..."); for (Vocabulary v : vocabManager.list()) { LOG.debug("Updating vocabulary " + v.getUriString()); vocabManager.updateIfChanged(v.getUriString()); } LOG.info("Updating content of all installed extensions..."); for (Extension ex : extensionManager.list()) { LOG.debug("Updating extension " + ex.getRowType()); extensionManager.updateIfChanged(ex.getRowType()); } } public void setExtension(Extension extension) { this.extension = extension; } /** * To hold the state transition request, so the same request triggered purely by a URL will not work. * * @param synchronise form variable */ public void setSynchronise(String synchronise) { this.synchronise = StringUtils.trimToNull(synchronise) != null; } public void setUrl(String url) { this.url = url; } /** * @return list of latest registered extensions */ public List<Extension> getLatestExtensionVersions() { return latestExtensionVersions; } public void setLatestExtensionVersions(List<Extension> latestExtensionVersions) { this.latestExtensionVersions = latestExtensionVersions; } /** * @return true if all installed extensions are the latest version, false otherwise */ public boolean isUpToDate() { return upToDate; } public void setUpToDate(boolean upToDate) { this.upToDate = upToDate; } }