001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.task;
018
019import static org.opends.messages.AdminToolMessages.*;
020import static org.opends.server.types.ExistingFileBehavior.*;
021import static org.opends.server.util.SchemaUtils.*;
022
023import java.io.File;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034
035import javax.naming.NamingException;
036import javax.naming.directory.BasicAttribute;
037import javax.naming.directory.DirContext;
038import javax.naming.directory.ModificationItem;
039import javax.swing.SwingUtilities;
040
041import org.forgerock.i18n.LocalizableMessage;
042import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
043import org.forgerock.opendj.ldap.ModificationType;
044import org.forgerock.opendj.ldap.schema.AttributeType;
045import org.forgerock.opendj.ldap.schema.ObjectClass;
046import org.forgerock.opendj.ldap.schema.SchemaBuilder;
047import org.forgerock.opendj.ldap.schema.SchemaElement;
048import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
049import org.opends.guitools.controlpanel.datamodel.SomeSchemaElement;
050import org.opends.guitools.controlpanel.ui.ColorAndFontConstants;
051import org.opends.guitools.controlpanel.ui.ProgressDialog;
052import org.opends.guitools.controlpanel.util.Utilities;
053import org.opends.server.config.ConfigConstants;
054import org.opends.server.core.DirectoryServer;
055import org.opends.server.types.Attributes;
056import org.opends.server.types.Entry;
057import org.opends.server.types.LDIFExportConfig;
058import org.opends.server.types.LDIFImportConfig;
059import org.opends.server.types.Modification;
060import org.opends.server.types.OpenDsException;
061import org.opends.server.types.Schema;
062import org.opends.server.util.LDIFReader;
063import org.opends.server.util.LDIFWriter;
064
065/** The task that is launched when a schema element must be deleted. */
066public class DeleteSchemaElementsTask extends Task
067{
068  /** The list of object classes that the user asked to delete. */
069  private Set<ObjectClass> providedOcsToDelete = new LinkedHashSet<>();
070  /** The list of attributes that the user asked to delete. */
071  private Set<AttributeType> providedAttrsToDelete = new LinkedHashSet<>();
072  /** The list of object classes that will be actually deleted (some might be recreated). */
073  private Set<ObjectClass> ocsToDelete = new LinkedHashSet<>();
074  /** The list of attributes that will be actually deleted (some might be recreated). */
075  private Set<AttributeType> attrsToDelete = new LinkedHashSet<>();
076  /** The list of object classes that will be recreated. */
077  private Set<ObjectClass> ocsToAdd = new LinkedHashSet<>();
078  /** The list of attributes that will be recreated. */
079  private Set<AttributeType> attrsToAdd = new LinkedHashSet<>();
080
081  /**
082   * Constructor of the task.
083   * @param info the control panel information.
084   * @param dlg the progress dialog where the task progress will be displayed.
085   * @param ocsToDelete the object classes that must be deleted (ordered).
086   * @param attrsToDelete the attributes that must be deleted (ordered).
087   */
088  public DeleteSchemaElementsTask(ControlPanelInfo info, ProgressDialog dlg,
089      Set<ObjectClass> ocsToDelete, Set<AttributeType> attrsToDelete)
090  {
091    super(info, dlg);
092
093    this.providedOcsToDelete.addAll(ocsToDelete);
094    this.providedAttrsToDelete.addAll(attrsToDelete);
095
096    Schema schema = info.getServerDescriptor().getSchema();
097    LinkedHashSet<AttributeType> allAttrsToDelete =
098      DeleteSchemaElementsTask.getOrderedAttributesToDelete(attrsToDelete,
099          schema);
100    LinkedHashSet<ObjectClass> allOcsToDelete = null;
101    if (!attrsToDelete.isEmpty())
102    {
103      allOcsToDelete =
104        DeleteSchemaElementsTask.getOrderedObjectClassesToDeleteFromAttrs(
105          attrsToDelete, schema);
106    }
107    if (!ocsToDelete.isEmpty())
108    {
109      LinkedHashSet<ObjectClass> orderedOCs =
110          DeleteSchemaElementsTask.getOrderedObjectClassesToDelete(ocsToDelete, schema);
111      if (allOcsToDelete == null)
112      {
113        allOcsToDelete = orderedOCs;
114      }
115      else
116      {
117        allOcsToDelete.addAll(orderedOCs);
118      }
119    }
120    ArrayList<AttributeType> lAttrsToDelete = new ArrayList<>(allAttrsToDelete);
121    for (int i = lAttrsToDelete.size() - 1; i >= 0; i--)
122    {
123      AttributeType attrToDelete = lAttrsToDelete.get(i);
124      if (!attrsToDelete.contains(attrToDelete))
125      {
126        AttributeType attrToAdd = getAttributeToAdd(attrToDelete);
127        if (attrToAdd != null)
128        {
129          attrsToAdd.add(attrToAdd);
130        }
131      }
132    }
133
134    assert allOcsToDelete != null;
135    ArrayList<ObjectClass> lOcsToDelete = new ArrayList<>(allOcsToDelete);
136    for (int i = lOcsToDelete.size() - 1; i >= 0; i--)
137    {
138      ObjectClass ocToDelete = lOcsToDelete.get(i);
139      if (!ocsToDelete.contains(ocToDelete))
140      {
141        ocsToAdd.add(getObjectClassToAdd(lOcsToDelete.get(i)));
142      }
143    }
144
145    this.ocsToDelete.addAll(allOcsToDelete);
146    this.attrsToDelete.addAll(allAttrsToDelete);
147  }
148
149  @Override
150  public Set<String> getBackends()
151  {
152    return Collections.emptySet();
153  }
154
155  @Override
156  public boolean canLaunch(Task taskToBeLaunched,
157      Collection<LocalizableMessage> incompatibilityReasons)
158  {
159    boolean canLaunch = true;
160    if (state == State.RUNNING &&
161        (taskToBeLaunched.getType() == Task.Type.DELETE_SCHEMA_ELEMENT ||
162         taskToBeLaunched.getType() == Task.Type.MODIFY_SCHEMA_ELEMENT ||
163         taskToBeLaunched.getType() == Task.Type.NEW_SCHEMA_ELEMENT))
164    {
165      incompatibilityReasons.add(getIncompatibilityMessage(this,
166            taskToBeLaunched));
167      canLaunch = false;
168    }
169    return canLaunch;
170  }
171
172  @Override
173  public Type getType()
174  {
175    return Type.NEW_SCHEMA_ELEMENT;
176  }
177
178  @Override
179  public void runTask()
180  {
181    state = State.RUNNING;
182    lastException = null;
183
184    try
185    {
186      updateSchema();
187      state = State.FINISHED_SUCCESSFULLY;
188    }
189    catch (Throwable t)
190    {
191      lastException = t;
192      state = State.FINISHED_WITH_ERROR;
193    }
194  }
195
196  @Override
197  protected String getCommandLinePath()
198  {
199    return null;
200  }
201
202  @Override
203  protected List<String> getCommandLineArguments()
204  {
205    return Collections.emptyList();
206  }
207
208  @Override
209  public LocalizableMessage getTaskDescription()
210  {
211    return INFO_CTRL_PANEL_DELETE_SCHEMA_ELEMENT_TASK_DESCRIPTION.get();
212  }
213
214  /**
215   * Updates the schema.
216   * @throws OpenDsException if an error occurs.
217   */
218  private void updateSchema() throws OpenDsException
219  {
220    final int totalNumber = ocsToDelete.size() + attrsToDelete.size();
221    int numberDeleted = 0;
222    for (ObjectClass objectClass : ocsToDelete)
223    {
224      final SomeSchemaElement element = new SomeSchemaElement(objectClass);
225      deleteSchemaElement(element, numberDeleted, totalNumber, INFO_CTRL_PANEL_DELETING_OBJECTCLASS);
226      numberDeleted++;
227    }
228
229    for (AttributeType attribute : attrsToDelete)
230    {
231      final SomeSchemaElement element = new SomeSchemaElement(attribute);
232      deleteSchemaElement(element, numberDeleted, totalNumber, INFO_CTRL_PANEL_DELETING_ATTRIBUTE);
233      numberDeleted++;
234    }
235
236    if (!ocsToAdd.isEmpty() || !attrsToAdd.isEmpty())
237    {
238      SwingUtilities.invokeLater(new Runnable()
239      {
240        @Override
241        public void run()
242        {
243          getProgressDialog().appendProgressHtml(Utilities.applyFont(
244              "<br><br>"+
245              INFO_CTRL_PANEL_EXPLANATION_TO_DELETE_REFERENCED_ELEMENTS.get()+
246              "<br><br>",
247              ColorAndFontConstants.progressFont));
248        }
249      });
250
251      NewSchemaElementsTask createTask =
252        new NewSchemaElementsTask(getInfo(), getProgressDialog(), ocsToAdd,
253            attrsToAdd);
254      createTask.runTask();
255    }
256  }
257
258  private void deleteSchemaElement(final SomeSchemaElement element, final int numberDeleted, final int totalNumber,
259      final Arg1<Object> deletingElementMsg) throws OnlineUpdateException, OpenDsException
260  {
261    SwingUtilities.invokeLater(new Runnable()
262    {
263      @Override
264      public void run()
265      {
266        final boolean isFirst = numberDeleted == 0;
267        if (!isFirst)
268        {
269          getProgressDialog().appendProgressHtml("<br><br>");
270        }
271        printEquivalentCommandToDelete(element);
272        getProgressDialog().appendProgressHtml(
273            Utilities.getProgressWithPoints(
274                deletingElementMsg.get(element.getNameOrOID()), ColorAndFontConstants.progressFont));
275      }
276    });
277
278    if (isServerRunning())
279    {
280      try
281      {
282        BasicAttribute attr = new BasicAttribute(element.getAttributeName());
283        attr.add(getSchemaFileAttributeValue(element));
284        ModificationItem mod = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attr);
285        getInfo().getConnection().getLdapContext().modifyAttributes(
286            ConfigConstants.DN_DEFAULT_SCHEMA_ROOT,
287            new ModificationItem[]  { mod });
288      }
289      catch (NamingException ne)
290      {
291        throw new OnlineUpdateException(ERR_CTRL_PANEL_ERROR_UPDATING_SCHEMA.get(ne), ne);
292      }
293    }
294    else
295    {
296      updateSchemaFile(element);
297    }
298
299    final int fNumberDeleted = numberDeleted + 1;
300    SwingUtilities.invokeLater(new Runnable()
301    {
302      @Override
303      public void run()
304      {
305        getProgressDialog().getProgressBar().setIndeterminate(false);
306        getProgressDialog().getProgressBar().setValue((fNumberDeleted * 100) / totalNumber);
307        getProgressDialog().appendProgressHtml(
308            Utilities.getProgressDone(ColorAndFontConstants.progressFont));
309      }
310    });
311  }
312
313  /**
314   * Updates the schema file by deleting the provided schema element.
315   * @param schemaElement the schema element to be deleted.
316   * @throws OpenDsException if an error occurs.
317   */
318  private void updateSchemaFile(SomeSchemaElement schemaElement) throws OpenDsException
319  {
320    String schemaFile = getSchemaFile(schemaElement);
321
322    try (LDIFExportConfig exportConfig = new LDIFExportConfig(schemaFile, OVERWRITE);
323        LDIFReader reader = new LDIFReader(new LDIFImportConfig(schemaFile)))
324    {
325      Entry schemaEntry = reader.readEntry();
326
327      Modification mod = new Modification(ModificationType.DELETE,
328          Attributes.create(
329              schemaElement.getAttributeName(),
330              getSchemaFileAttributeValue(schemaElement)));
331      schemaEntry.applyModification(mod);
332      try (LDIFWriter writer = new LDIFWriter(exportConfig))
333      {
334        writer.writeEntry(schemaEntry);
335        exportConfig.getWriter().newLine();
336      }
337    }
338    catch (IOException e)
339    {
340      throw new OfflineUpdateException(
341          ERR_CTRL_PANEL_ERROR_UPDATING_SCHEMA.get(e), e);
342    }
343  }
344
345  /**
346   * Returns the schema file for a given schema element.
347   * @param element the schema element.
348   * @return the schema file for a given schema element.
349   */
350  private String getSchemaFile(SomeSchemaElement element)
351  {
352    String schemaFile = element.getSchemaFile();
353    if (schemaFile == null)
354    {
355      schemaFile = ConfigConstants.FILE_USER_SCHEMA_ELEMENTS;
356    }
357    File f = new File(schemaFile);
358    if (!f.isAbsolute())
359    {
360      f = new File(
361          DirectoryServer.getEnvironmentConfig().getSchemaDirectory(),
362          schemaFile);
363    }
364    return f.getAbsolutePath();
365  }
366
367  /**
368   * Returns the value in the schema file for the provided element.
369   * @param element the schema element.
370   * @return the value in the schema file for the provided element.
371   */
372  private String getSchemaFileAttributeValue(SomeSchemaElement element)
373  {
374    return element.toString();
375  }
376
377  /**
378   * Prints the equivalent command-line to delete the schema element in the
379   * progress dialog.
380   * @param element the schema element to be deleted.
381   */
382  private void printEquivalentCommandToDelete(SomeSchemaElement element)
383  {
384    String schemaFile = getSchemaFile(element);
385    String attrName = element.getAttributeName();
386    String attrValue = getSchemaFileAttributeValue(element);
387
388    String msg;
389    if (!isServerRunning())
390    {
391      msg = getEquivalentCommandOfflineMsg(element, schemaFile)
392          + "<br><b>" + attrName + ": " + attrValue + "</b><br><br>";
393    }
394    else
395    {
396      ArrayList<String> args = new ArrayList<>();
397      args.add("-a");
398      args.addAll(getObfuscatedCommandLineArguments(
399          getConnectionCommandLineArguments(true, true)));
400      args.add(getNoPropertiesFileArgument());
401      String equiv = getEquivalentCommandLine(getCommandLinePath("ldapmodify"), args);
402
403      msg = getEquivalentCommandOnlineMsg(element)
404          + "<br><b>" + equiv + "<br>"
405          + "dn: cn=schema<br>"
406          + "changetype: modify<br>"
407          + "delete: " + attrName + "<br>"
408          + attrName + ": " + attrValue
409          + "</b>"
410          + "<br><br>";
411    }
412    getProgressDialog().appendProgressHtml(Utilities.applyFont(msg, ColorAndFontConstants.progressFont));
413  }
414
415  private LocalizableMessage getEquivalentCommandOfflineMsg(SomeSchemaElement element, String schemaFile)
416  {
417    if (element.isAttributeType())
418    {
419      return INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_DELETE_ATTRIBUTE_OFFLINE.get(element.getNameOrOID(), schemaFile);
420    }
421    return INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_DELETE_OBJECTCLASS_OFFLINE.get(element.getNameOrOID(), schemaFile);
422  }
423
424  private LocalizableMessage getEquivalentCommandOnlineMsg(SomeSchemaElement element)
425  {
426    if (element.isAttributeType())
427    {
428      return INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_DELETE_ATTRIBUTE_ONLINE.get(element.getNameOrOID());
429    }
430    return INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_DELETE_OBJECTCLASS_ONLINE.get(element.getNameOrOID());
431  }
432
433  private AttributeType getAttributeToAdd(AttributeType attrToDelete)
434  {
435    boolean isSuperior = false;
436    for (AttributeType attr : providedAttrsToDelete)
437    {
438      if (attr.equals(attrToDelete.getSuperiorType()))
439      {
440        isSuperior = true;
441        break;
442      }
443    }
444    if (isSuperior)
445    {
446       // get a new attribute without the superior type
447       return SomeSchemaElement.changeSuperiorType(attrToDelete, null);
448    }
449    else
450    {
451      // Nothing to be changed in the definition of the attribute itself.
452      return attrToDelete;
453    }
454  }
455
456  private ObjectClass getObjectClassToAdd(ObjectClass ocToDelete)
457  {
458    boolean containsAttribute = containsAttributeToDelete(ocToDelete);
459    boolean hasSuperior = false;
460    Set<ObjectClass> newSuperiors = new LinkedHashSet<>();
461    for (ObjectClass sup : ocToDelete.getSuperiorClasses())
462    {
463      boolean isFound = false;
464      for (ObjectClass oc: providedOcsToDelete)
465      {
466        if(sup.equals(oc))
467        {
468          hasSuperior = true;
469          isFound = true;
470          newSuperiors.addAll(getNewSuperiors(oc));
471          break;
472        }
473      }
474      if (!isFound)
475      {
476        //Use the same super if not found in the list.
477        newSuperiors.add(sup);
478      }
479    }
480
481    if (containsAttribute || hasSuperior)
482    {
483      Map<String, List<String>> extraProperties = cloneExtraProperties(ocToDelete);
484      Set<AttributeType> required;
485      Set<AttributeType> optional;
486      if (containsAttribute)
487      {
488        required = new HashSet<>(ocToDelete.getDeclaredRequiredAttributes());
489        optional = new HashSet<>(ocToDelete.getDeclaredOptionalAttributes());
490        required.removeAll(providedAttrsToDelete);
491        optional.removeAll(providedAttrsToDelete);
492      }
493      else
494      {
495        required = ocToDelete.getDeclaredRequiredAttributes();
496        optional = ocToDelete.getDeclaredOptionalAttributes();
497      }
498      final String oid = ocToDelete.getOID();
499      final Schema schema = getInfo().getServerDescriptor().getSchema();
500      return new SchemaBuilder(schema.getSchemaNG()).buildObjectClass(oid)
501          .names(ocToDelete.getNames())
502          .description(ocToDelete.getDescription())
503          .superiorObjectClasses(getNameOrOIDsForOCs(newSuperiors))
504          .requiredAttributes(getNameOrOIDsForATs(required))
505          .optionalAttributes(getNameOrOIDsForATs(optional))
506          .type(ocToDelete.getObjectClassType())
507          .obsolete(ocToDelete.isObsolete())
508          .extraProperties(extraProperties)
509          .addToSchema()
510          .toSchema()
511          .getObjectClass(oid);
512    }
513    else
514    {
515      // Nothing to be changed in the definition of the object class itself.
516      return ocToDelete;
517    }
518  }
519
520  private boolean containsAttributeToDelete(ObjectClass ocToDelete)
521  {
522    for (AttributeType attr : providedAttrsToDelete)
523    {
524      if (ocToDelete.getRequiredAttributes().contains(attr)
525          || ocToDelete.getOptionalAttributes().contains(attr))
526      {
527        return true;
528      }
529    }
530    return false;
531  }
532
533  private Set<ObjectClass> getNewSuperiors(ObjectClass currentSup)
534  {
535    Set<ObjectClass> newSuperiors = new LinkedHashSet<>();
536    if (currentSup.getSuperiorClasses() != null &&
537        !currentSup.getSuperiorClasses().isEmpty())
538    {
539      for (ObjectClass o : currentSup.getSuperiorClasses())
540      {
541        if (providedOcsToDelete.contains(o))
542        {
543          newSuperiors.addAll(getNewSuperiors(o));
544        }
545        else
546        {
547          newSuperiors.add(o);
548        }
549      }
550    }
551    return newSuperiors;
552  }
553
554  /**
555   * Returns an ordered set of the attributes that must be deleted.
556   * @param attrsToDelete the attributes to be deleted.
557   * @param schema the server schema.
558   * @return an ordered list of the attributes that must be deleted.
559   */
560  public static LinkedHashSet<AttributeType> getOrderedAttributesToDelete(
561      Collection<AttributeType> attrsToDelete, Schema schema)
562  {
563    LinkedHashSet<AttributeType> orderedAttributes = new LinkedHashSet<>();
564    for (AttributeType attribute : attrsToDelete)
565    {
566      orderedAttributes.addAll(getOrderedChildrenToDelete(attribute, schema));
567      orderedAttributes.add(attribute);
568    }
569    return orderedAttributes;
570  }
571
572  /**
573   * Returns an ordered list of the object classes that must be deleted.
574   * @param ocsToDelete the object classes to be deleted.
575   * @param schema the server schema.
576   * @return an ordered list of the object classes that must be deleted.
577   */
578  public static LinkedHashSet<ObjectClass> getOrderedObjectClassesToDelete(
579      Collection<ObjectClass> ocsToDelete, Schema schema)
580  {
581    LinkedHashSet<ObjectClass> orderedOcs = new LinkedHashSet<>();
582    for (ObjectClass oc : ocsToDelete)
583    {
584      orderedOcs.addAll(getOrderedChildrenToDelete(oc, schema));
585      orderedOcs.add(oc);
586    }
587    return orderedOcs;
588  }
589
590  /**
591   * Returns an ordered list of the object classes that must be deleted when
592   * deleting a list of attributes that must be deleted.
593   * @param attrsToDelete the attributes to be deleted.
594   * @param schema the server schema.
595   * @return an ordered list of the object classes that must be deleted when
596   * deleting a list of attributes that must be deleted.
597   */
598  public static LinkedHashSet<ObjectClass>
599  getOrderedObjectClassesToDeleteFromAttrs(
600      Collection<AttributeType> attrsToDelete, Schema schema)
601  {
602    LinkedHashSet<ObjectClass> orderedOcs = new LinkedHashSet<>();
603    ArrayList<ObjectClass> dependentClasses = new ArrayList<>();
604    for (AttributeType attr : attrsToDelete)
605    {
606      for (ObjectClass oc : schema.getObjectClasses())
607      {
608        if (oc.getRequiredAttributes().contains(attr)
609            || oc.getOptionalAttributes().contains(attr))
610        {
611          dependentClasses.add(oc);
612        }
613      }
614    }
615    for (ObjectClass oc : dependentClasses)
616    {
617      orderedOcs.addAll(getOrderedChildrenToDelete(oc, schema));
618      orderedOcs.add(oc);
619    }
620    return orderedOcs;
621  }
622
623  /**
624   * Clones the extra properties of the provided schema element.  This can
625   * be used when copying schema elements.
626   * @param element the schema element.
627   * @return the extra properties of the provided schema element.
628   */
629  public static Map<String, List<String>> cloneExtraProperties(SchemaElement element)
630  {
631    Map<String, List<String>> extraProperties = new HashMap<>();
632    Map<String, List<String>> props = element.getExtraProperties();
633    for (String name : props.keySet())
634    {
635      extraProperties.put(name, new ArrayList<>(props.get(name)));
636    }
637    return extraProperties;
638  }
639
640  private static LinkedHashSet<AttributeType> getOrderedChildrenToDelete(
641      AttributeType attribute, Schema schema)
642  {
643    LinkedHashSet<AttributeType> children = new LinkedHashSet<>();
644    for (AttributeType attr : schema.getAttributeTypes())
645    {
646      if (attribute.equals(attr.getSuperiorType()))
647      {
648        children.addAll(getOrderedChildrenToDelete(attr, schema));
649        children.add(attr);
650      }
651    }
652    return children;
653  }
654
655  private static LinkedHashSet<ObjectClass> getOrderedChildrenToDelete(
656      ObjectClass objectClass, Schema schema)
657  {
658    LinkedHashSet<ObjectClass> children = new LinkedHashSet<>();
659    for (ObjectClass oc : schema.getObjectClasses())
660    {
661      if (oc.getSuperiorClasses().contains(objectClass))
662      {
663        children.addAll(getOrderedChildrenToDelete(oc, schema));
664        children.add(oc);
665      }
666    }
667    return children;
668  }
669}