/* * Copyright 2012 * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology * Technische Universität Darmstadt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.tudarmstadt.ukp.clarin.webanno.ui.project; import static org.apache.commons.collections.CollectionUtils.isEmpty; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.reflect.ConstructorUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.OnChangeAjaxBehavior; import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy; import org.apache.wicket.extensions.ajax.markup.html.tabs.AjaxTabbedPanel; import org.apache.wicket.extensions.markup.html.tabs.AbstractTab; import org.apache.wicket.extensions.markup.html.tabs.ITab; import org.apache.wicket.markup.html.form.Button; import org.apache.wicket.markup.html.form.CheckBox; import org.apache.wicket.markup.html.form.ChoiceRenderer; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.ListChoice; import org.apache.wicket.markup.html.form.RadioChoice; import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.markup.html.form.upload.FileUpload; import org.apache.wicket.markup.html.form.upload.FileUploadField; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import org.springframework.security.core.context.SecurityContextHolder; import org.wicketstuff.annotation.mount.MountPath; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectLifecycleAware; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectLifecycleAwareRegistry; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.SecurityUtil; import de.tudarmstadt.ukp.clarin.webanno.automation.service.AutomationService; import de.tudarmstadt.ukp.clarin.webanno.constraints.ConstraintsService; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.ProjectPermission; import de.tudarmstadt.ukp.clarin.webanno.model.ScriptDirection; import de.tudarmstadt.ukp.clarin.webanno.model.Tag; import de.tudarmstadt.ukp.clarin.webanno.model.TagSet; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.Role; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil; import de.tudarmstadt.ukp.clarin.webanno.support.ZipUtils; import de.tudarmstadt.ukp.clarin.webanno.support.dialog.ChallengeResponseDialog; import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaAjaxLink; import de.tudarmstadt.ukp.clarin.webanno.ui.core.menu.MenuItem; import de.tudarmstadt.ukp.clarin.webanno.ui.core.menu.MenuItemCondition; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.ApplicationPageBase; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.NameUtil; import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelBase; import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelRegistryService; import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelRegistryService.ProjectSettingsPanelDecl; /** * This is the main page for Project Settings. The Page has Four Panels. The * {@link AnnotationGuideLinePanel} is used to update documents to a project. The * {@code ProjectDetailsPanel} used for updating Project details such as descriptions of a project * and name of the Project The {@link ProjectTagSetsPanel} is used to add {@link TagSet} and * {@link Tag} details to a Project as well as updating them The {@link ProjectUsersPanel} is used * to update {@link User} to a Project */ @MenuItem(icon="images/setting_tools.png", label="Projects", prio=400) @MountPath("/projectsetting.html") public class ProjectPage extends ApplicationPageBase { private static final long serialVersionUID = -2102136855109258306L; private static final Logger LOG = LoggerFactory.getLogger(ProjectPage.class); private @SpringBean AnnotationSchemaService annotationService; private @SpringBean AutomationService automationService; private @SpringBean DocumentService documentService; private @SpringBean ProjectService projectService; private @SpringBean ConstraintsService constraintsService; private @SpringBean UserDao userRepository; private @SpringBean ProjectSettingsPanelRegistryService projectSettingsPanelRegistryService; private @SpringBean ProjectLifecycleAwareRegistry projectLifecycleAwareRegistry; public static ProjectSelectionForm projectSelectionForm; public static ProjectDetailForm projectDetailForm; private ImportProjectForm importProjectForm; public static boolean exportInProgress = false; public ProjectPage() { setModel(Model.of(new ProjectPageState())); projectSelectionForm = new ProjectSelectionForm("projectSelectionForm", getModel()); projectDetailForm = new ProjectDetailForm("projectDetailForm", new Model<>()); importProjectForm = new ImportProjectForm("importProjectForm"); add(projectSelectionForm); add(importProjectForm); add(projectDetailForm); MetaDataRoleAuthorizationStrategy.authorize(importProjectForm, Component.RENDER, "ROLE_ADMIN"); } public void setModel(IModel<ProjectPageState> aModel) { setDefaultModel(aModel); } @SuppressWarnings("unchecked") public IModel<ProjectPageState> getModel() { return (IModel<ProjectPageState>) getDefaultModel(); } public void setModelObject(ProjectPageState aModel) { setDefaultModelObject(aModel); } public ProjectPageState getModelObject() { return (ProjectPageState) getDefaultModelObject(); } class ProjectSelectionForm extends Form<ProjectPageState> { private static final long serialVersionUID = -1L; private Button createProject; public ProjectSelectionForm(String id, IModel<ProjectPageState> aModel) { super(id, CompoundPropertyModel.of(aModel)); add(createProject = new Button("create", new StringResourceModel("label")) { private static final long serialVersionUID = 1L; @Override public void onSubmit() { Project project = new Project(); project.setMode(projectService.listProjectTypes().get(0).id()); projectDetailForm.setModelObject(project); ProjectSelectionForm.this.getModelObject().project = null; } }); MetaDataRoleAuthorizationStrategy.authorize( createProject, Component.RENDER, StringUtils.join(new String[] { Role.ROLE_ADMIN.name(), Role.ROLE_PROJECT_CREATOR.name() }, ",")); add(new ListChoice<Project>("project") { private static final long serialVersionUID = 1L; { setChoices(new LoadableDetachableModel<List<Project>>() { private static final long serialVersionUID = 1L; @Override protected List<Project> load() { String username = SecurityContextHolder.getContext().getAuthentication() .getName(); User user = userRepository.get(username); return projectService.listAccessibleProjects(user); } }); setChoiceRenderer(new ChoiceRenderer<Project>("name")); setNullValid(false); add(new OnChangeAjaxBehavior() { private static final long serialVersionUID = 1L; @Override protected void onUpdate(AjaxRequestTarget aTarget) { if (getModelObject() != null) { projectDetailForm.setModelObject( ProjectSelectionForm.this.getModelObject().project); projectDetailForm.allTabs.setSelectedTab(0); aTarget.add(projectDetailForm); } } }); } @Override protected CharSequence getDefaultChoice(String aSelectedValue) { return ""; } }); } } static public class ProjectPageState implements Serializable { private static final long serialVersionUID = 502442621850380752L; public Project project; public List<String> documents; public List<String> permissionLevels; } private class ImportProjectForm extends Form<ImportProjectFormState> { private static final long serialVersionUID = -6361609153142402692L; private FileUploadField fileUpload; @SuppressWarnings({ "unchecked", "rawtypes" }) public ImportProjectForm(String id) { super(id, new CompoundPropertyModel<>(new ImportProjectFormState())); add(new CheckBox("generateUsers")); add(fileUpload = new FileUploadField("content", new Model())); add(new Button("importProject", new StringResourceModel("label")) { private static final long serialVersionUID = 1L; @Override public void onSubmit() { List<FileUpload> exportedProjects = fileUpload.getFileUploads(); if (isEmpty(exportedProjects)) { error("Please choose appropriate project/s in zip format"); } else { actionImportProject(exportedProjects, ImportProjectForm.this.getModelObject().generateUsers); } } }); } } private static class ImportProjectFormState implements Serializable { private static final long serialVersionUID = -5858027181097577052L; boolean generateUsers = true; } public class ProjectDetailForm extends Form<Project> { private static final long serialVersionUID = -1L; private AjaxTabbedPanel<ITab> allTabs; public ProjectDetailForm(String id, IModel<Project> aModel) { super(id, CompoundPropertyModel.of(aModel)); add(allTabs = makeTabs()); setMultiPart(true); setOutputMarkupPlaceholderTag(true); } @Override protected void onConfigure() { super.onConfigure(); setVisible(getModelObject() != null); } private AjaxTabbedPanel<ITab> makeTabs() { List<ITab> tabs = new ArrayList<>(); tabs.add(new AbstractTab(Model.of("Details")) { private static final long serialVersionUID = 6703144434578403272L; @Override public Panel getPanel(String panelId) { return new ProjectDetailsPanel(panelId); } @Override public boolean isVisible() { return !exportInProgress; } }); // Add the project settings panels from the registry for (ProjectSettingsPanelDecl psp : projectSettingsPanelRegistryService.getPanels()) { AbstractTab tab = new AbstractTab(Model.of(psp.label)) { private static final long serialVersionUID = -1503555976570640065L; @Override public Panel getPanel(String aPanelId) { try { ProjectSettingsPanelBase panel = (ProjectSettingsPanelBase) ConstructorUtils .invokeConstructor(psp.panel, aPanelId, ProjectDetailForm.this.getModel()); return panel; } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean isVisible() { IModel<Project> model = ProjectDetailForm.this.getModel(); return model.getObject() != null && model.getObject().getId() != 0 && psp.condition.applies(model.getObject(), exportInProgress); } }; tabs.add(tab); } AjaxTabbedPanel<ITab> tabsPanel = new AjaxTabbedPanel<ITab>("tabs", tabs); tabsPanel.setOutputMarkupPlaceholderTag(true); tabsPanel.setOutputMarkupId(true); return tabsPanel; } } private class ProjectDetailsPanel extends Panel { private static final long serialVersionUID = 1118880151557285316L; private ChallengeResponseDialog deleteProjectDialog; private LambdaAjaxLink deleteProjectLink; private RadioChoice<String> projectType; public ProjectDetailsPanel(String id) { super(id); TextField<String> projectNameTextField = new TextField<String>("name"); projectNameTextField.setRequired(true); add(projectNameTextField); add(new TextArea<String>("description").setOutputMarkupPlaceholderTag(true)); add(projectType = new RadioChoice<String>("mode", projectService.listProjectTypes() .stream().map(t -> t.id()).collect(Collectors.toList())) { private static final long serialVersionUID = -8268365384613932108L; @Override protected void onConfigure() { super.onConfigure(); IModel<Project> model = projectDetailForm.getModel(); setEnabled(model.getObject() != null && model.getObject().getId() == 0); } }); add(new DropDownChoice<ScriptDirection>("scriptDirection", Arrays.asList(ScriptDirection.values()))); add(new CheckBox("disableExport")); add(new Button("save", new StringResourceModel("label")) { private static final long serialVersionUID = 1L; @Override public void validate() { super.validate(); if (!NameUtil.isNameValid(projectNameTextField.getInput())) { error("Project name shouldn't contain characters such as /\\*?&!$+[^]"); LOG.error("Project name shouldn't contain characters such as /\\*?&!$+[^]"); } if (projectNameTextField.getModelObject()!=null && projectService.existsProject(projectNameTextField.getInput()) && !projectNameTextField.getInput().equals(projectNameTextField.getModelObject())) { error("Another project with same name exists. Please try a different name"); } } @Override public void onSubmit() { Project project = projectDetailForm.getModelObject(); if (!NameUtil.isNameValid(project.getName())) { // Maintain already loaded project and selected Users // Hence Illegal Project modification (limited // privilege, illegal // project // name,...) preserves the original one if (project.getId() != 0) { project.setName(ImportUtil.validName(project.getName())); } error("Project name shouldn't contain characters such as /\\*?&!$+[^]"); LOG.error("Project name shouldn't contain characters such as /\\*?&!$+[^]"); return; } // if (repository.existsProject(project.getName()) && project.getId() == 0) { // error("Another project with name [" + project.getName() + "] exists"); // return; // } if (projectService.existsProject(project.getName()) && project.getId() != 0) { error("project updated with name [" + project.getName() + "]"); return; } if (project.getId() == 0) { try { String username = SecurityContextHolder.getContext().getAuthentication() .getName(); projectService.createProject(project); projectService.createProjectPermission(new ProjectPermission(project, username, PermissionLevel.ADMIN)); projectService.createProjectPermission(new ProjectPermission(project, username, PermissionLevel.CURATOR)); projectService.createProjectPermission( new ProjectPermission(project, username, PermissionLevel.USER)); annotationService.initializeTypesForProject(project); projectSelectionForm.getModelObject().project = project; } catch (IOException e) { error("Project repository path not found " + ":" + ExceptionUtils.getRootCauseMessage(e)); LOG.error("Project repository path not found " + ":" + ExceptionUtils.getRootCauseMessage(e)); } } else { projectService.updateProject(project); } } }); IModel<String> projectNameModel = PropertyModel.of(projectDetailForm.getModel(), "name"); add(deleteProjectDialog = new ChallengeResponseDialog("deleteProjectDialog", new StringResourceModel("DeleteProjectDialog.title", this), new StringResourceModel("DeleteProjectDialog.text", this) .setModel(projectDetailForm.getModel()).setParameters(projectNameModel), projectNameModel)); add(deleteProjectLink = new LambdaAjaxLink("deleteProjectLink", this::actionDeleteProject) { private static final long serialVersionUID = -7483337091365688847L; @Override protected void onConfigure() { super.onConfigure(); Project project = projectDetailForm.getModelObject(); setVisible(project != null && project.getId() != 0); } }); add(new Button("cancel", new StringResourceModel("label")) { private static final long serialVersionUID = 1L; { // Avoid saving data setDefaultFormProcessing(false); } @Override public void onSubmit() { projectSelectionForm.getModelObject().project = null; projectDetailForm.setModelObject(null); } }); } private void actionDeleteProject(AjaxRequestTarget aTarget) { deleteProjectDialog.setConfirmAction((target) -> { Project project = projectDetailForm.getModelObject(); try { projectService.removeProject(project); projectDetailForm.setModelObject(null); projectSelectionForm.getModelObject().project = null; target.add(ProjectPage.this); } catch (IOException e) { LOG.error("Unable to remove project :" + ExceptionUtils.getRootCauseMessage(e)); error("Unable to remove project " + ":" + ExceptionUtils.getRootCauseMessage(e)); target.addChildren(getPage(), FeedbackPanel.class); } }); deleteProjectDialog.show(aTarget); } } private void actionImportProject(List<FileUpload> exportedProjects, boolean aGenerateUsers) { Project importedProject = new Project(); // import multiple projects! for (FileUpload exportedProject : exportedProjects) { InputStream tagInputStream; try { tagInputStream = exportedProject.getInputStream(); if (!ZipUtils.isZipStream(tagInputStream)) { error("Invalid ZIP file"); return; } File zipFfile = exportedProject.writeToTempFile(); if (!ImportUtil.isZipValidWebanno(zipFfile)) { error("Incompatible to webanno ZIP file"); } ZipFile zip = new ZipFile(zipFfile); InputStream projectInputStream = null; for (Enumeration zipEnumerate = zip.entries(); zipEnumerate.hasMoreElements();) { ZipEntry entry = (ZipEntry) zipEnumerate.nextElement(); if (entry.toString().replace("/", "").startsWith(ImportUtil.EXPORTED_PROJECT) && entry.toString().replace("/", "").endsWith(".json")) { projectInputStream = zip.getInputStream(entry); break; } } // Load the project model from the JSON file String text = IOUtils.toString(projectInputStream, "UTF-8"); de.tudarmstadt.ukp.clarin.webanno.export.model.Project importedProjectSetting = JSONUtil .getJsonConverter() .getObjectMapper() .readValue(text, de.tudarmstadt.ukp.clarin.webanno.export.model.Project.class); // Import the project itself importedProject = ImportUtil.createProject(importedProjectSetting, projectService); // Import additional project things projectService.onProjectImport(zip, importedProjectSetting, importedProject); // Import missing users if (aGenerateUsers) { ImportUtil.createMissingUsers(importedProjectSetting, userRepository); } // Notify all relevant service so that they can initialize themselves for the given project for (ProjectLifecycleAware bean : projectLifecycleAwareRegistry.getBeans()) { try { bean.onProjectImport(zip, importedProjectSetting, importedProject); } catch (IOException e) { throw e; } catch (Exception e) { throw new IllegalStateException(e); } } // Import layers Map<de.tudarmstadt.ukp.clarin.webanno.export.model.AnnotationFeature, AnnotationFeature> featuresMap = ImportUtil .createLayer(importedProject, importedProjectSetting, userRepository, annotationService); /* * for (TagSet tagset : importedProjectSetting.getTagSets()) { * ImportUtil.createTagset(importedProject, tagset, projectRepository, * annotationService); } */ // Import source document ImportUtil.createSourceDocument(importedProjectSetting, importedProject, documentService, featuresMap); // Import source document content ImportUtil.createSourceDocumentContent(zip, importedProject, documentService); // Import automation settings ImportUtil.createMiraTemplate(importedProjectSetting, automationService, featuresMap); // Import annotation document content ImportUtil.createAnnotationDocument(importedProjectSetting, importedProject, documentService); // Import annotation document content ImportUtil.createAnnotationDocumentContent(zip, importedProject, documentService); // Import curation document content ImportUtil.createCurationDocumentContent(zip, importedProject, documentService); } catch (Exception e) { error("Error Importing Project " + ExceptionUtils.getRootCauseMessage(e)); LOG.error("Error importing project", e); } } projectDetailForm.setModelObject(importedProject); ProjectPageState selectedProjectModel = new ProjectPageState(); selectedProjectModel.project = importedProject; projectSelectionForm.setModelObject(selectedProjectModel); RequestCycle.get().setResponsePage(getPage()); } /** * Only admins and project managers can see this page */ @MenuItemCondition public static boolean menuItemCondition(ProjectService aRepo, UserDao aUserRepo) { String username = SecurityContextHolder.getContext().getAuthentication().getName(); User user = aUserRepo.get(username); return SecurityUtil.projectSettingsEnabeled(aRepo, user); } }