public function duplicate()
    {
        $class = get_class($this);
        $new_object = new $class();
        $id_field = new SwatDBField($this->id_field, 'integer');

        // public properties
        $properties = array_merge(
            $this->getPublicProperties(),
            $this->getSerializableProtectedProperties()
        );

        foreach ($properties as $name => $value) {
            if ($name !== $id_field->name) {
                $new_object->$name = $this->$name;
            }
        }

        // sub-dataobjects
        foreach ($this->sub_data_objects as $name => $object) {
            $saver_method =
                'save' . str_replace(' ', '', ucwords(strtr($name, '_', ' ')));

            $object->setDatabase($this->db);
            if (method_exists($this, $saver_method)) {
                $new_object->$name = $object->duplicate();
            } elseif (!array_key_exists($name, $this->internal_properties)) {
                $new_object->$name = $object;
            }
        }

        // internal properties
        foreach ($this->internal_properties as $name => $value) {
            if (
                !(
                    array_key_exists(
                        $name,
                        $this->internal_property_accessible
                    ) && $this->internal_property_accessible[$name]
                )
            ) {
                continue;
            }

            $autosave = $this->internal_property_autosave[$name];

            if ($this->hasSubDataObject($name)) {
                $object = $this->getSubDataObject($name);
                if ($autosave) {
                    $new_object->$name = $object->duplicate();
                } else {
                    $new_object->$name = $object;
                }
            } else {
                $new_object->$name = $value;
            }
        }

        $new_object->setDatabase($this->db);

        return $new_object;
    }